触发器本身不直接死锁但会放大死锁风险:执行跨表DML、未走索引查询或AFTER中更新主表,极易引发ABBA循环死锁;识别方法包括查NEW./OLD.语句、锁序相反及tables in use>1。

触发器本身不直接死锁,但它是死锁的放大器和隐藏推手——只要它执行了任何跨表 DML、未走索引的查询、或在 AFTER 中更新业务主表,就极大概率把原本可控的锁等待,变成 ABBA 循环死锁。
怎么看死锁日志里是不是触发器干的
别只扫一眼报错 SQL,重点盯 SHOW ENGINE INNODB STATUS\G 输出中 LATEST DETECTED DEADLOCK 块里的两个事务:
- 找是否出现带
NEW.或OLD.的语句,比如UPDATE order_log SET status = 'processed' WHERE order_id = NEW.id—— 这基本就是 AFTER INSERT 触发器执行的痕迹 - 看
HOLDS THE LOCK(S)和WAITING FOR THIS LOCK TO BE GRANTED是否跨了不同表,且顺序相反:事务 A 持orders锁等user_points,事务 B 持user_points锁等orders - 检查
TRANSACTION段里mysql tables in use数值是否大于 1(如2),说明至少涉及两张表,触发器参与概率极高
AFTER 触发器为什么比 BEFORE 更危险
AFTER 触发器在新行已插入并持 X 锁后才执行,此时再对其他行或表做 DML,等于在已有锁基础上叠加新锁请求,锁链直接拉长:
-
AFTER INSERT中执行UPDATE stats SET count = count + 1 WHERE type = 'order'—— 若type无索引,InnoDB 全表扫描 + 加 Next-Key Lock,瞬间锁住整张统计表 -
AFTER UPDATE中写日志表时用了WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)—— RR 隔离下会锁整个时间区间,不是单行 - 多级嵌套(如
AFTER INSERT → UPDATE user_points → BEFORE UPDATE points_audit)会让锁路径不可控,8.0+ 才能在状态里准确定位内层语句
触发器里哪些操作必须立刻砍掉
以下行为在高并发下几乎等于主动制造死锁热点:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 跨业务主表的
UPDATE:例如从orders触发器去改users余额表 —— 一律移出触发器,改用应用层统一锁序 -
WHERE条件未命中主键或唯一索引的任何 DML:比如UPDATE config SET value = 'on' WHERE module = 'payment',若module无索引,就是全表 X 锁 - 带范围条件的
SELECT FOR UPDATE:如SELECT * FROM logs WHERE created_at > ... FOR UPDATE—— RR 下锁整个区间,极易撞上其他事务 - 调用存储过程:除非该过程明确声明只读且无任何 DML;否则锁范围不可控,排查成本陡增
真正管用的优化动作清单
别只调隔离级别或加重试,优先做这几件事:
- 所有触发器内
SELECT必须能被EXPLAIN判定为type=const或ref;禁用type=ALL或type=index—— 后者是锁爆炸前兆 - 用
INSERT INTO summary_log () VALUES ()替代同步更新汇总表;引擎选BLACKHOLE或CSV,再由定时任务批量聚合 - BEFORE 中禁止
UPDATE t1 SET x=1 WHERE id = NEW.id—— 改用SET NEW.x = 1直接赋值,不额外加锁 - 彻底禁用
SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE;配置类数据优先应用层缓存或预加载到变量
最易被忽略的一点:触发器逻辑哪怕只查一行,也必须确保那个字段有唯一索引;否则 SELECT qty FROM stock WHERE sku = NEW.sku 在并发 INSERT 时,可能因间隙锁互相阻塞 —— 这种问题在线上压测根本复现不了,只有高峰时段才爆发。










