触发器中禁止操作同一张表或显式开启事务,避免死锁;跨表更新应移至应用层或异步处理;优先使用BEFORE触发器;禁用嵌套触发器;排查需结合慢日志与调试日志。

触发器里别碰别的表,尤其别更新同一张表
MySQL 触发器(BEFORE UPDATE 或 AFTER INSERT)执行时,当前语句持有的行锁还没释放,如果触发器里再用 UPDATE、DELETE 去操作同一张表(甚至只是查 SELECT ... FOR UPDATE),极大概率触发死锁——因为 InnoDB 会尝试加新锁,而原事务锁和触发器内锁互相等待。
常见错误写法:
CREATE TRIGGER tr_update_log AFTER INSERT ON orders FOR EACH ROW BEGIN UPDATE order_stats SET total = total + NEW.amount WHERE date = CURDATE(); -- 死锁高发点 END;
解决思路:
- 把跨表更新逻辑移到应用层,由业务代码统一控制事务边界
- 若必须统计,改用异步方式:插入后往消息队列发事件,另起进程处理
- 真要留在 DB 层,用
INSERT ... ON DUPLICATE KEY UPDATE替代SELECT + UPDATE,减少锁持有时间
触发器里禁止显式开启事务或调用存储过程含事务
MySQL 不允许在触发器中执行 BEGIN...END 外的 START TRANSACTION、COMMIT 或 ROLLBACK,否则直接报错 ERROR 1305 (42000): SAVEPOINT does not exist 或更隐蔽的死锁。更危险的是:某些封装好的存储过程内部用了事务,被触发器调用时会干扰主事务的锁生命周期。
检查方法:
- 用
SHOW CREATE PROCEDURE proc_name看是否含START TRANSACTION或SAVEPOINT - 避免在触发器中调用任何未确认“无事务”的自定义存储过程
- 所有数据变更尽量用单条 DML,不拆成多步带条件判断的语句
慎用 AFTER 触发器做级联更新,优先选 BEFORE
AFTER 触发器在原语句提交后才执行,此时行锁已释放,看似安全;但若它引发另一条修改语句(比如更新关联表),而该语句又恰好与并发事务形成循环等待链(如事务 A 更新 users 后触发更新 logs,事务 B 先锁 logs 再试图更新 users),死锁就发生了。
更稳妥的做法:
- 能用
BEFORE完成的逻辑(如校验、字段补全、生成 UUID)全放BEFORE - 级联写入类操作,改用应用层批量处理,或通过外键
ON UPDATE CASCADE(仅限简单一对一) - 确需
AFTER,只做无锁操作:写入日志表(INSERT INTO audit_log)、发通知、调用 UDF(确保不连库)
监控和复现死锁不能只看 SHOW ENGINE INNODB STATUS
触发器引发的死锁往往藏得深:SHOW ENGINE INNODB STATUS 只显示最近一次死锁,且不标注哪段是触发器代码。实际排查时容易误判为应用 SQL 问题。
有效手段:
- 开启 MySQL 慢日志并加
--log-short-format,配合log_output = TABLE,查mysql.slow_log中带TRIGGER关键字的记录 - 在触发器开头加
INSERT INTO debug_log VALUES (NOW(), USER(), 'tr_orders_insert', @@TRX_ID);,用@@TRX_ID关联INFORMATION_SCHEMA.INNODB_TRX - 压测时用
pt-deadlock-logger持续捕获,比人工刷INNODB STATUS可靠得多
最麻烦的是嵌套触发器——A 表触发器改 B 表,B 表又有触发器改 C 表,锁链一长,路径分析几乎只能靠日志打点。别轻易上多层触发逻辑。










