mysql触发器不开启新事务,而是复用主事务上下文,其dml操作隐式延长锁持有时间、打乱加锁顺序,并在rr隔离下因无索引扫描+间隙锁+跨表操作叠加引发死锁;show engine innodb status不显示触发器sql,因其被归入父事务,仅体现外层语句和表级锁信息,需通过死锁日志中同一非主表同时出现在holds/waiting两侧、trigger_debug日志及data_locks交叉验证定位。

MySQL 本身不支持嵌套事务,触发器里的 DML 不会开启新事务,而是直接复用主事务的上下文——所以所谓“隐式嵌套事务死锁”,本质是主事务被触发器拖入更复杂的锁路径,最终在 RR 隔离级别下因间隙锁+无索引扫描+跨表操作三者叠加引爆死锁。
为什么 SHOW ENGINE INNODB STATUS 看不到触发器 SQL
触发器内执行的 UPDATE、SELECT FOR UPDATE 等语句,不会作为独立事务出现在 INNODB_TRX 或死锁日志中。InnoDB 把它视为父事务的一部分,只记录外层语句(如 INSERT INTO orders)和最终持有的表级锁信息。这意味着:
- 死锁日志里两个事务都只显示
INSERT,但实际卡住的是触发器对t_audit或t_stock的更新 -
performance_schema.data_lock_waits中,BLOCKING_ENGINE_TRANSACTION_ID对应的事务在INNODB_TRX里查不到trx_query—— 因为它早已执行完主语句,正卡在触发器逻辑里 - 真正线索藏在死锁日志的
HOLDS THE LOCK(S)和WAITING FOR THIS LOCK TO BE GRANTED两栏:如果同一张非主表(比如t_log)同时出现在两边,基本可断定是触发器在作祟
如何确认某次阻塞确实来自触发器
别靠猜,用三步交叉验证:
- 临时把触发器改成
BEFORE INSERT+SET NEW.invalid_col = 'x'(故意写不存在字段),看是否报错Unknown column—— 报了说明真被调用 - 在触发器开头加一句
INSERT INTO trigger_debug VALUES (NOW(), USER(), NEW.id, 'audit_update');,观察该表是否有插入记录且时间吻合 - 关闭自动提交:
SET autocommit = 0,手动执行一条会触发它的INSERT,再立刻查performance_schema.data_locks WHERE OBJECT_NAME = 't_audit'—— 如果锁确实存在,且没其他显式语句操作这张表,就是它
触发器里哪些写法必然放大死锁风险
不是所有触发器都危险,但以下模式几乎必出问题:
-
WHERE条件走全表扫描:比如UPDATE t_log SET status = 'done' WHERE module = 'payment',而module字段没索引 → 锁住成百上千行甚至整个间隙 - 范围条件 +
FOR UPDATE:如SELECT * FROM t_history WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 DAY) FOR UPDATE,RR 下会锁住整个时间区间 - 跨表更新形成 ABBA:主事务锁
orders→ 触发器去锁product_stock;另一并发事务反向操作,死锁立刻成立 - 嵌套触发器链:A 触发 B,B 再触发 C,锁顺序彻底不可控,
EXPLAIN都救不了
真正有效的缓解手段不是禁用,而是切掉锁冲突路径
禁用触发器等于放弃业务逻辑,正确做法是让触发器“轻量、确定、可预测”:
- 对触发器内每条
UPDATE/SELECT FOR UPDATE,用真实参数跑EXPLAIN FORMAT=TRADITIONAL,重点盯type字段:出现ALL或index必须建索引;key为空或rows远大于实际影响行数,说明优化器误判,间隙锁正在乱加 - 全局或会话级降级隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,能干掉 RR 下最要命的间隙锁,代价是可能不可重复读——但库存扣减、日志落库这类场景完全可接受 - 应用层必须集成重试:捕获错误码
1213后重放整个事务,重试间隔递增(如0.1 * (attempt + 1)秒),且只用于幂等操作;别指望连接池(如 HikariCP)自动处理,它默认不识别1213
最难的从来不是加索引或改配置,而是意识到触发器不是帮你省事的黑盒——它是主事务的延伸,每一行 DML 都在继承锁生命周期。忽略这点,再细的优化也挡不住高并发下的锁爆炸。











