触发器本身不直接导致死锁,但会在主事务中隐式执行多条DML,共享同一事务ID和锁生命周期,因加锁顺序不一致(如主SQL走PK、触发器走非唯一索引)、子查询引发间隙锁、调用存储过程扩大锁范围等,极易形成循环等待。

触发器本身不直接导致死锁,但会在事务中隐式引入额外的锁操作,且执行时机不可控——这是死锁高频温床。
为什么触发器容易引发死锁
触发器在 INSERT/UPDATE/DELETE 执行后、事务提交前自动触发,它所执行的语句会复用当前事务上下文,共享同一事务 ID 和锁生命周期。问题在于:
- 你写的业务 SQL 可能按
PRIMARY KEY顺序加锁,但触发器里的一条UPDATE t_log SET status = 'done' WHERE order_id = ?却走idx_order_id,两个索引路径不一致 → 锁顺序错乱 - 触发器内含子查询(如
SELECT balance FROM accounts WHERE user_id = NEW.user_id)时,InnoDB 可能对子查询结果提前加 gap lock,而主事务又在更新同一张表的其他行 → 形成等待环 - 触发器调用存储过程或访问其他表,进一步扩大锁范围,且开发者通常不会在写触发器时主动考虑锁兼容性
如何用 SHOW ENGINE INNODB STATUS 定位触发器相关死锁
死锁发生后立刻执行 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中的lock_mode X locks rec but not gap或lock_mode X locks gap before rec,若其中一把锁来自t_log或audit_history这类日志表,基本可判定是触发器行为 - 注意
INSERT INTO t_order后紧跟着UPDATE t_log的堆栈顺序 —— 这就是触发器执行痕迹
避免触发器死锁的实操策略
根本原则:让触发器的锁行为可预测、可收敛。不要指望“删掉触发器”来解耦,而是控制它的锁粒度和路径:
- 触发器内所有 DML 必须走**唯一、确定的索引**:比如统一用
PRIMARY KEY更新,禁用WHERE status = 'pending'这类范围条件(会触发 gap lock) - 把原本在触发器里做的
INSERT INTO audit_log改为异步写入:用应用层发 MQ 或写入轻量队列表(INSERT DELAYED已废弃,改用INSERT IGNORE INTO log_buffer (...) VALUES (...)+ 定时 flush) - 如果必须同步记录,先在主事务开头用
SELECT ... FOR UPDATE预锁触发器要操作的行(例如SELECT id FROM t_log WHERE order_id = NEW.order_id FOR UPDATE),确保锁顺序与触发器内一致 - 禁止在触发器里调用函数或存储过程——它们可能隐式开启新事务或改变隔离级别,使锁行为失控
事务隔离级别调整是否有效
对触发器场景,SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED 能减少 gap lock,但风险明显:
- 它只对触发器内普通 SELECT 有效;若触发器含
SELECT ... FOR UPDATE,照样加 record lock,死锁照常发生 - 更危险的是:主事务用 REPEATABLE READ 保证一致性快照,触发器却降级到 READ COMMITTED → 触发器读到的数据可能已被其他事务修改,导致逻辑错乱(例如扣款前查余额,触发器查到旧值)
- SQL Server 用户注意:
READ_COMMITTED_SNAPSHOT = ON可缓解,但仅限于该数据库下所有READ COMMITTED事务,对触发器内显式指定的隔离级别无效
真正关键的不是隔离级别,而是**所有锁操作是否落在同一索引路径、是否按固定顺序获取**——这点被忽略最多,也最难排查。










