SQL触发器本身不直接死锁,但会隐式扩展单条SQL为多步DML并塞入同一事务,导致锁不释放、顺序不可控、范围爆炸,使死锁从“可能”变为“大概率”;AFTER触发器对主表执行UPDATE会二次申请同一行X锁引发循环等待,而BEFORE仅修改NEW字段安全;死锁日志中tables in use/locked大于1、HOLDS与WAITING表名不一致、executionStack出现INSERT紧接UPDATE等痕迹可快速识别触发器参与;跨表UPDATE、无索引WHERE、范围SELECT FOR UPDATE及调用非只读存储过程等操作必须移出触发器。

SQL触发器本身不直接死锁,但它会把单条业务 SQL 隐式扩展成多步 DML,并全部塞进同一个事务里——锁不释放、顺序不可控、范围易爆炸,死锁就从“可能”变成“大概率”。
为什么 AFTER 触发器一执行 UPDATE 同表就卡住
主事务已对 NEW.id 所在行持 X 锁,触发器再发 UPDATE t1 SET status = 'done' WHERE id = NEW.id,等于二次申请同一行锁。InnoDB 不允许同事务内重复加锁,直接形成循环等待。
- BEFORE 触发器中用
SET NEW.status = 'done'是安全的——不走额外 SQL,不额外加锁 - AFTER 触发器里任何对主表的 DML(哪怕只改一行)都极可能触发死锁
- 批量插入时,每个新行都会触发一次该逻辑,锁竞争呈线性放大
死锁日志里怎么一眼认出是触发器干的
执行 SHOW ENGINE INNODB STATUS\G 后,盯紧 LATEST DETECTED DEADLOCK 区块:
- 看
TRANSACTION段是否出现mysql tables in use 2, locked 2——数字大于 1 就说明至少涉及两张表,大概率有触发器参与 - 比对
HOLDS THE LOCK(S)和WAITING FOR THIS LOCK TO BE GRANTED中的表名:若其中一把锁落在t_log、audit_history这类日志表上,基本可判定是触发器行为 - 堆栈顺序里出现
INSERT INTO t_order紧接着UPDATE t_log,这就是触发器执行痕迹
哪些触发器操作必须立刻砍掉
这些写法在高并发下几乎等于主动制造死锁热点:
- 跨业务主表的
UPDATE,比如订单触发器去改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
真正难的是锁路径不可见
你很难从应用日志里看出某次 INSERT 背后悄悄锁了三张表。排查时别只盯错误码 1205 或 1213,得进死锁图 XML 里的 executionStack 找触发器名;优化时也别只调隔离级别或加重试——索引缺失、跨表更新、嵌套触发器这三类问题不解决,死锁只会换个姿势回来。

















