触发器引发死锁可从三处快速识别:一是LATEST DETECTED DEADLOCK中出现NEW./OLD.语法的DML;二是跨表反向加锁链;三是非主业务表上的隐式DML。

直接看 SHOW ENGINE INNODB STATUS\G 里的 LATEST DETECTED DEADLOCK 区块,重点抓带 NEW. 或 OLD. 的语句、跨表反向加锁链、非主业务表上的隐式 DML——这三处对上,基本就是触发器在拖后腿。
怎么从死锁日志里一眼认出是触发器惹的祸
别信应用层报错那条 SQL,真正埋雷的在 InnoDB 状态输出里:
-
LATEST DETECTED DEADLOCK中出现含NEW.id、OLD.status这类语法的语句,比如UPDATE order_log SET order_id = NEW.id WHERE id = NEW.log_id,100% 来自触发器 - 两个事务的
holds the locks和waiting for this lock涉及不同表,且顺序相反:事务 A 持orders锁等user_points,事务 B 持user_points锁等orders - 某个事务在日志里执行了明显不属于主业务流的 DML,比如主表是
payments,但死锁链里却有UPDATE points_audit——而你代码里根本没写这条 - MySQL 8.0+ 可看到嵌套触发器内层语句;低版本只显示最外层 SQL,实际卡点可能藏在
AFTER INSERT → 触发 UPDATE → 再触发 BEFORE UPDATE的第二层里
AFTER 和 BEFORE 触发器,哪个更危险
AFTER INSERT 是死锁高发区,BEFORE INSERT 相对干净——不是语法轻,是锁行为本质不同:
-
AFTER INSERT执行时,新行已落盘并持有 X 锁,事务还没提交;此时再跑UPDATE stats SET count = count + 1 WHERE type = 'order',等于在已有锁上叠新锁请求,锁等待链直接拉长 -
BEFORE INSERT只能改NEW字段值,不产生额外 DML,也就不会引入新锁(除非你手贱写了SELECT ... FOR UPDATE) -
AFTER UPDATE同样危险:原行已加 X 锁,再更新其他表,极易形成跨表等待 -
BEFORE UPDATE改NEW字段安全,但若里面调用存储过程或查其他表做判断,仍可能触发隐式锁
触发器里哪些操作必须立刻砍掉
以下行为在并发稍高时,几乎必然导致锁等待超时甚至死锁,不能靠“加索引”补救,得移出触发器:
- 跨业务主表的
UPDATE,例如从orders触发器里去改users表余额——一律改用应用层统一锁序(如先SELECT ... FOR UPDATEusers,再处理 orders) -
WHERE条件未命中主键或唯一索引的任何 DML,比如UPDATE config SET value = 'on' WHERE module = 'payment',若module列无索引,就是全表 X 锁 - 带范围条件的
SELECT ... FOR UPDATE,如SELECT * FROM logs WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 DAY) FOR UPDATE,在REPEATABLE READ下会锁住整个时间区间 - 调用存储过程,除非该过程明确声明只读、无任何 DML、不访问业务表——否则锁路径不可控
真正难缠的不是触发器语法本身,而是它把锁行为藏在事务内部,让加锁顺序、锁持有时间、锁覆盖范围全部变得不可见。排查时最容易忽略的是:你以为只锁了一行,其实触发器悄悄锁了另一张表的整列;你以为事务很快结束,其实它在等一个你完全没意识到的隐式锁释放。


















