触发器本身不直接死锁,但会隐式延长锁持有时间、打乱加锁顺序、引发锁升级,导致死锁更频繁且难排查;关键证据是死锁日志中LATEST DETECTED DEADLOCK区块显示跨表反向等待(如事务A持orders锁等user_points,事务B反之),或出现含NEW.的UPDATE语句,且触发器内SQL未走索引。

触发器本身不直接死锁,但会让死锁更频繁、更难排查——核心问题在于它隐式延长锁持有时间、打乱锁顺序、且容易因索引缺失引发锁升级。
怎么看死锁日志里是不是触发器惹的祸
别只扫一眼报错 SQL,重点看 SHOW ENGINE INNODB STATUS\G 输出中 LATEST DETECTED DEADLOCK 块里的两个事务:
- 找 “
UPDATE” 或 “SELECT ... FOR UPDATE” 语句是否出现在触发器目标表上(比如主表是orders,死锁链里却有UPDATE user_points SET ... WHERE user_id = ?) - 检查 “
holds the locks” 和 “waiting for this lock” 是否跨了不同表,且顺序相反(如事务 A 持有orders锁等待user_points,事务 B 持有user_points锁等待orders) - 若日志里出现类似
UPDATE order_log SET status = 'processed' WHERE order_id = NEW.id这种带NEW.的语句,基本可断定是AFTER INSERT触发器执行的
为什么 AFTER 触发器比 BEFORE 更危险
AFTER 触发器在新行已插入并加 X 锁后才执行,此时再对其他行或表做 DML,等于在已有锁基础上叠加新锁请求,锁等待链直接拉长:
-
AFTER INSERT中执行UPDATE stats SET count = count + 1 WHERE type = 'order'—— 若type无索引,InnoDB 全表扫描 + 加 Next-Key Lock,瞬间锁住整张统计表 -
BEFORE INSERT只能改NEW字段值,不额外加锁(除非你主动写SELECT ... FOR UPDATE) - 多级触发器嵌套(如
AFTER INSERT→ 更新user_points→ 触发BEFORE UPDATE再写points_audit)会让锁路径不可控,8.0+ 才能在INNODB STATUS里准确定位到内层语句
触发器里哪些操作必须立刻砍掉
以下行为在高并发下几乎等于主动制造死锁热点:
- 跨业务主表的
UPDATE(例如从orders触发器去改users余额表),一律移出触发器,改用应用层统一锁序 - 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
真正管用的优化动作清单
别只调隔离级别或加重试,优先做这几件事:
- 对触发器内所有 SQL 执行
EXPLAIN FORMAT=TRADITIONAL,确认key字段非 NULL、type不是ALL或index,否则立刻补索引 - 把统计类逻辑(如计数器、汇总字段)全部移出触发器,改用应用层异步更新或定时任务;非得实时,就用
BEFORE INSERT/UPDATE直接算好赋给NEW.count_field - 若必须保留
AFTER触发器更新关联表,强制要求 WHERE 条件必须是主键等值匹配(WHERE id = ?),禁用任何范围、IN、LIKE 查询 - 在 my.cnf 中开启
innodb_print_all_deadlocks = 1,否则你只能看到最后一次死锁,错过关键模式
最常被忽略的一点:触发器锁行为完全取决于它内部 SQL 的执行计划,而不是语法看起来“只是查一下”。哪怕一行 SELECT,没索引就是全表锁——而这种锁,在死锁日志里往往只显示为“某事务卡在 user_points 表”,没人会想到是触发器里一句没加索引的查询干的。

















