确认触发器引发死锁需重点分析show engine innodb status\g中latest detected deadlock区块:检查两事务是否跨表反向加锁(如a持orders锁等user_points、b持user_points锁等orders),并查找含new.或old.的sql(如update order_log set status=... where order_id = new.id),若触发器对非主业务表执行dml且未走索引或含范围锁,则极可能为根因。

怎么确认死锁日志里是触发器在作祟
别只盯着报错 SQL,重点看 SHOW ENGINE INNODB STATUS\G 输出中 LATEST DETECTED DEADLOCK 区块的两个事务细节:
- 检查
waiting for this lock和holds the locks是否跨了不同表,且顺序相反(比如事务 A 持有orders锁、等待user_points;事务 B 持有user_points、等待orders)——这是典型触发器引发的反向加锁链 - 找有没有带
NEW.或OLD.的语句,例如UPDATE order_log SET status = 'processed' WHERE order_id = NEW.id,基本可断定出自AFTER INSERT触发器 - 若发现某个事务在触发器目标表上执行了
UPDATE或SELECT FOR UPDATE,但该表并非主业务表(比如订单插入后去改积分表),大概率是触发器越界操作
AFTER vs BEFORE:哪个更容易引发锁等待超时
AFTER INSERT 是高危区,BEFORE INSERT 相对安全,原因很实在:
-
AFTER INSERT执行时,新行已落盘并持有了行级 X 锁,事务还没提交;此时再执行其他 DML(如更新统计表),等于在已有锁基础上叠加新锁请求,锁持有时间被隐式拉长 -
BEFORE INSERT只能修改NEW字段值,不产生额外 DML,也就不会引入新锁(除非你主动写SELECT FOR UPDATE) - MySQL 8.0+ 才能在
INNODB STATUS中准确定位多级嵌套触发器里的内层语句;低版本里,你看到的“阻塞 SQL”往往只是最外层,真正卡点藏在触发器里
触发器里哪些操作会立刻拖慢整个事务
以下行为在并发稍高时,几乎必然导致锁等待超时甚至死锁:
- 跨业务主表的
UPDATE,例如从orders触发器里去改users表余额——一律移出触发器,改用应用层统一锁序(如先锁 user,再锁 order) -
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,在 RR 隔离下会锁住整个时间区间 - 调用存储过程,除非该过程明确声明只读、无任何 DML,否则锁路径不可控
真正有效的排查动作清单
别一上来就调 innodb_lock_wait_timeout 或加重试,优先做这几件事:
- 立刻查
information_schema.INNODB_TRX,按trx_started排序,揪出运行超过 5 秒的长事务——触发器常因逻辑卡顿或异常退出导致事务悬停 - 开
innodb_print_all_deadlocks = ON,把所有死锁写进错误日志,避免SHOW ENGINE INNODB STATUS被覆盖 - 用
performance_schema.data_locks(MySQL 8.0+)查当前锁分布,重点关注触发器涉及的表是否出现大量RECORD LOCK或NEXT-KEY LOCK - 把统计类、日志类、跨表关联类逻辑全部从触发器里砍掉,改用应用层异步更新或定时任务——这是最省事也最彻底的解法
触发器的问题不在语法错,而在它把锁逻辑藏得太深:你看不到它什么时候加锁、锁多久、锁哪几行。一旦出问题,得靠日志反推,而不是靠代码直读。











