MySQL触发器本身不直接引起死锁,但会显著加剧死锁频次与排查难度;根本原因在于其DML操作被纳入主事务锁上下文,未走索引导致锁升级、AFTER触发器叠加新锁、跨表操作引发ABBA锁序、RR隔离下间隙锁泛滥。

MySQL触发器本身不直接“引起”死锁,但它会让死锁更频繁、更难定位——根本原因在于它把额外的 DML 操作塞进了主事务的锁上下文里,而你往往意识不到这些 SQL 正在用同样的锁资源排队。
触发器内 DML 未走索引 → 锁升级成全表扫描
这是高频死锁最隐蔽也最致命的源头。比如触发器里有 UPDATE product_stock SET qty = qty - 1 WHERE sku_code = NEW.sku,但 sku_code 没建索引,InnoDB 就会全表扫描 + 加 Next-Key Lock,瞬间锁住整张表的间隙和记录。
- 用
EXPLAIN FORMAT=TRADITIONAL SELECT * FROM product_stock WHERE sku_code = 'ABC123' FOR UPDATE验证:如果type是ALL或index,必须加索引 -
key字段为空、rows估算值远大于实际行数,说明优化器误判,间隙锁已乱加 - 哪怕只影响 1 行,没索引也会锁几百上千行——这不是慢,是锁爆炸
AFTER 触发器执行时已持 X 锁 → 叠加新锁请求
AFTER INSERT 触发器运行时,新行已被插入并持有行级 X 锁,事务还没提交。此时再执行任何 DML(比如更新统计表),等于在已有锁基础上申请新锁,极易触发循环等待。
-
BEFORE INSERT只能改NEW字段,基本不加新锁;AFTER则完全相反 - 日志里若出现
UPDATE order_log SET status = 'processed' WHERE order_id = NEW.id这类带NEW.的语句,基本可断定是AFTER触发器干的 - 多级嵌套(AFTER → BEFORE UPDATE → 再触发另一层)会让锁路径失控,MySQL 8.0+ 才能在
SHOW ENGINE INNODB STATUS\G中准确定位内层语句
跨表操作 + 锁序不一致 → 构成 ABBA 循环
订单表 orders 的触发器去改用户余额表 users,就是典型 ABBA:事务 A 持 orders 锁等 users,事务 B 持 users 锁等 orders。这种跨业务主表的 DML 必须砍掉。
- 禁止触发器内
UPDATE同一张表(自更新),InnoDB 已持锁又申请锁,纯循环等待 - 所有业务路径中表操作顺序必须严格一致,例如总是先
product_stock再warehouse_log - 涉及多表更新时,在
BEFORE触发器中仅赋值NEW字段,把真正 DML 移到应用层统一控制锁序
RR 隔离级别下间隙锁泛滥 → 范围条件秒变死锁热点
REPEATABLE READ 是 MySQL 默认隔离级别,它的间隙锁(Gap Lock)在触发器里尤其危险。比如 WHERE status IN ('pending', 'processing'),InnoDB 会锁住整个区间,而不是只锁匹配行。
- 降到
READ COMMITTED后,这类条件只锁匹配行,不锁间隙——代价是可能不可重复读,但库存扣减、日志写入等场景通常可接受 - 单会话测试:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED - 全局生效:在
my.cnf加transaction_isolation = READ-COMMITTED,重启 MySQL
真正难的不是加索引或调隔离级别,而是意识到触发器里每一行 SQL 都在主事务的锁上下文中——它不是独立小任务,而是主事务锁链上新增的一环。一旦漏掉某条 DML 的索引、或放任跨表操作,死锁就从偶发变成常态。


















