触发器本身不直接导致死锁,但会显著放大死锁风险;其作为宿主事务一部分复用锁上下文,若执行非预期锁操作(如无索引更新、范围查询加间隙锁、唯一键冲突隐式锁),易引发死锁。

触发器本身不直接导致死锁,但会显著放大死锁风险
MySQL 触发器(BEFORE/AFTER INSERT/UPDATE/DELETE)在事务内隐式执行,它不是独立事务,而是宿主事务的一部分。这意味着触发器里的 SQL 语句会复用当前事务的锁上下文——它加的锁、扫描的索引范围、等待的资源,全部计入原事务的锁持有图中。一旦触发器执行了非预期的锁操作(比如没走索引的 UPDATE、范围 SELECT ... FOR UPDATE),就可能把原本安全的单行更新,变成多行/间隙锁竞争,从而诱发死锁。
常见触发器引发死锁的 3 种实操场景
以下都是真实线上高频踩坑点,注意触发器逻辑是否无意中引入了「反序加锁」或「隐式锁升级」:
-
跨表更新 + 无索引条件:比如
AFTER UPDATE触发器里执行UPDATE log_table SET status=1 WHERE user_id = NEW.user_id AND type='pay',但log_table(user_id, type)缺少联合索引 → 全表扫描 → 升级为表锁 → 与主表行锁冲突 -
范围查询 + 间隙锁扩散:触发器中执行
SELECT * FROM orders WHERE user_id = NEW.user_id AND created_at > DATE_SUB(NOW(), INTERVAL 1 DAY) FOR UPDATE→ 在 RR 隔离级别下自动加Next-Key Lock→ 锁住未来可能插入的间隙 → 并发插入新订单时被阻塞,若另一事务也走同样路径,极易形成环形等待 -
唯一键冲突引发隐式 S/X 锁:触发器里
INSERT INTO user_cache (user_id, data) VALUES (NEW.user_id, '...'),而user_cache表有唯一索引;当并发插入相同user_id时,InnoDB 会对冲突值加隐式 X 锁,此时若两个事务又分别持有对方需要的其他行锁,死锁瞬间成立
如何验证触发器是否参与了死锁链
不能只看主 SQL,必须查完整死锁日志,重点盯触发器相关线索:
- 执行
SHOW ENGINE INNODB STATUS\G,定位LATEST DETECTED DEADLOCK区域 - 检查每个事务的
TRANSACTION段落中的mysql tables in use和locked tables—— 若出现触发器涉及的表名(哪怕主 SQL 没显式访问),说明它参与了加锁 - 看
HELD LOCKS和WAITING FOR THIS LOCK TO BE GRANTED中的索引名和记录值,比对触发器 SQL 的WHERE条件是否命中同一索引范围 - 用
performance_schema.data_locks实时抓取:触发器执行期间,OBJECT_SCHEMA和OBJECT_NAME字段会出现触发器所操作的表,LOCK_DATA可能显示间隙值(如32, 35),这就是间隙锁证据
规避触发器死锁的硬性建议
这不是“优化技巧”,而是生产环境必须遵守的底线:
- 触发器内禁止任何
SELECT ... FOR UPDATE或UPDATE操作,除非你能 100% 确保其 WHERE 条件走**覆盖索引 + 精确匹配**(即只锁单行 Record Lock) - 所有触发器涉及的 DML 语句,必须在对应表上建立**最左前缀匹配的联合索引**,且避免
LIKE '%xxx'、OR、函数包裹字段等破坏索引使用的写法 - 高并发写场景(如订单、支付)中,直接放弃触发器,改用应用层异步任务(如消息队列)完成旁路逻辑 —— 触发器的原子性代价,在分布式系统里往往得不偿失
- 如果必须用,开启
innodb_deadlock_detect=ON(默认已开),并确保业务代码捕获ERROR 1213 (40001)后做幂等重试,而不是静默失败
真正难的不是写触发器,是预判它在并发压力下会以什么方式抢锁、锁谁、锁多大范围。很多死锁日志里看不到触发器名字,但它早就在锁等待图里埋好了回路节点。










