直接看SHOW ENGINE INNODB STATUS\G中LATEST DETECTED DEADLOCK区块:若两事务跨表反向加锁(如A持orders锁等user_points、B持user_points锁等orders),且含NEW./OLD.语句(如UPDATE order_log SET status='done' WHERE order_id=NEW.id),即可确认是AFTER触发器引发的死锁。

怎么看死锁日志里是不是触发器在卡住
直接看 SHOW ENGINE INNODB STATUS\G 输出中 LATEST DETECTED DEADLOCK 区块,别扫一眼报错 SQL 就跳过——重点找两处:
- 两个事务的
holds the locks和waiting for this lock是否跨了不同表,且顺序相反(比如事务 A 持orders锁等user_points,事务 B 持user_points锁等orders) - 有没有带
NEW.或OLD.的语句,例如UPDATE order_log SET status = 'done' WHERE order_id = NEW.id——这种基本就是AFTER INSERT触发器干的
MySQL 5.7 只显示最外层语句,真正卡点藏在触发器里;8.0+ 才能准确定位到内层 DML。如果日志里主表是 orders,但锁链里反复出现 user_points 或 points_audit,八成是触发器跨表更新惹的祸。
怎么确认触发器里的 SQL 走不走索引
触发器里一条没索引的 WHERE 就能让行锁升级为全表 X 锁,不是慢,是锁爆炸。对触发器中每条 DML,用真实参数跑 EXPLAIN FORMAT=TRADITIONAL:
EXPLAIN SELECT * FROM product_stock WHERE sku_code = 'ABC123' FOR UPDATE- 重点看
type字段:出现ALL或index(全索引扫描)必须建索引 -
key为空、rows估算值远大于实际影响行数,说明优化器误判,间隙锁会乱加
特别注意:触发器里 UPDATE config SET value = 'on' WHERE module = 'payment',如果 module 没索引,InnoDB 就得全表扫描,所有扫描过的行都加 X 锁——哪怕只改一行。
为什么 AFTER 触发器比 BEFORE 更容易锁死
AFTER INSERT 是高危区,BEFORE INSERT 相对安全,原因很实在:
-
AFTER INSERT执行时,新行已落盘并持有了行级 X 锁,事务还没提交;此时再执行其他 DML(比如更新统计表),等于在已有锁基础上叠加新锁请求,锁持有时间被隐式拉长 -
BEFORE INSERT只能修改NEW字段值,不产生额外 DML,也就不会引入新锁(除非你主动写SELECT FOR UPDATE) - 多级嵌套触发器(如
AFTER INSERT → UPDATE user_points → BEFORE UPDATE → INSERT points_audit)会让锁路径不可控,8.0+ 才能在状态里定位到内层语句
高频场景下,AFTER 触发器里任何跨表操作、范围查询或存储过程调用,都是死锁热点。
怎么快速抓出正在阻塞的触发器语句
别只查应用层日志——触发器执行的 SQL 不会出现在业务代码里。用这三张系统表串联定位:
- 先查
SELECT * FROM information_schema.INNODB_TRX,找状态为LOCK WAIT的事务和它对应的trx_mysql_thread_id - 再查
SELECT * FROM information_schema.INNODB_LOCK_WAITS,拿到阻塞它的blocking_trx_id - 最后查
SELECT * FROM information_schema.INNODB_TRX WHERE trx_id = 'xxx',看trx_query字段——如果显示的是类似UPDATE order_log SET ... WHERE order_id = NEW.id,就坐实了
真正难的不是看到那行 SQL,而是意识到它根本不是你写的,而是由触发器自动补上的。很多团队花几天排查“谁在改积分表”,结果发现源头是一条没人维护的 AFTER INSERT 触发器。


















