触发器本身不直接导致死锁,但会放大锁冲突风险;因其在主事务中隐式执行,共享同一锁生命周期,一旦含UPDATE、SELECT FOR UPDATE、跨表操作或未走索引的查询,极易引发ABBA锁序和间隙锁冲突,导致死锁。

触发器本身不直接导致死锁,但它在事务中隐式执行时会放大锁冲突风险——只要里面出现 UPDATE、SELECT FOR UPDATE、INSERT INTO SELECT 或跨表操作,死锁概率就急剧上升。
BEFORE/AFTER触发器里为什么不能UPDATE同一张表
看似“只改自己”的操作,在InnoDB中极易触发循环等待:主事务已对 NEW.id 所在行持X锁,触发器再发一条 UPDATE t1 SET status = 'done' WHERE id = NEW.id,就是二次申请同一行锁。
- 正确做法是用
SET NEW.status = 'done'在BEFORE中直接赋值,不走额外SQL - 如果涉及多字段联动(如更新订单状态同时刷新子表计数),必须剥离出触发器,改由应用层异步完成
- 真要数据库侧维护统计值,可用
INSERT INTO summary_log写日志表(引擎选BLACKHOLE),再由定时任务批量聚合
触发器内SELECT FOR UPDATE或跨表DML为什么危险
死锁日志里出现 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: TABLE: `inventory`,基本可断定是触发器越界操作了——主事务锁订单表,触发器又去锁库存表,另一条路径反着来,立刻 deadlock。
- 彻底禁用
SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE - 真要查关联数据,只允许基于主键或唯一索引的单行查找,例如
SELECT qty FROM stock WHERE sku = NEW.sku(且sku必须有唯一索引) - 所有读操作必须命中覆盖索引:用
EXPLAIN确认type是const或ref,不能是ALL或index
为什么MAX(id)、LAST_INSERT_ID()在BEFORE INSERT里是雷区
并发 INSERT 时,多个 BEFORE INSERT 触发器同时执行 SELECT MAX(id) FROM orders,会在主键索引间隙上产生冲突的 gap lock,尤其在 RR 隔离级别下,互相阻塞概率极高。
- 绝对不要用
SELECT MAX(id)生成业务单号 - 改用
UUID_SHORT()、雪花ID,或应用层预分配段(如从seq_pool表UPDATE ... LIMIT 1取号) - 自增主键本身已由InnoDB保证唯一性,触发器无需参与ID生成逻辑
索引缺失和隔离级别怎么放大死锁风险
触发器里一条没走索引的 UPDATE product_stock SET qty = qty - 1 WHERE sku_code = 'ABC123',在 RR 下可能升级为全表间隙锁;而另一事务按相反顺序操作,ABBA 锁序瞬间形成。
- 对触发器内每条 DML,用真实参数模拟执行并加
EXPLAIN FORMAT=TRADITIONAL,确认是否走了预期索引 - 在 RR 下,
WHERE status IN ('pending', 'processing')这类范围条件会锁住整个区间;切到READ COMMITTED后,仅锁匹配行,间隙锁大幅收缩 - 代价是可能产生不可重复读,但对库存、日志、统计类业务通常可接受
真正容易被忽略的是:触发器共享主事务的锁生命周期,且执行计划不受隔离级别调控——优化重点从来不是“关掉它”,而是砍掉其中任何一次非必要锁申请、杜绝任何一次未命中索引的扫描、封死任何一次跨表写入的路径。

















