MySQL触发器中直接更新同一张表极易导致死锁,因InnoDB已持锁而触发器又申请新锁形成循环等待;应改用BEFORE触发器中SET NEW赋值、应用层异步处理或多表更新时统一加锁顺序。

触发器里直接更新同一张表会死锁
MySQL 的 BEFORE UPDATE 或 AFTER UPDATE 触发器如果对当前正在被修改的表执行 UPDATE、DELETE 或带 WHERE 的 SELECT ... FOR UPDATE,极大概率触发死锁——因为 InnoDB 在语句执行过程中已持有了行锁或间隙锁,触发器又试图获取新的锁,形成循环等待。
常见错误现象:Deadlock found when trying to get lock; try restarting transaction 频繁报出,尤其在并发更新同一主键范围或索引范围时。
- 别在触发器里写
UPDATE t1 SET status = 'done' WHERE id = NEW.id这类语句,哪怕看起来“只是改自己” - 如果必须同步更新其他字段,优先用
SET NEW.xxx = ...在BEFORE触发器中赋值,不走额外 DML - 涉及多行计算(比如统计子表数量)且需存到主表,应改用应用层异步刷新,或用延迟更新(如写入临时日志表 + 定时任务)
触发器调用存储过程也可能放大死锁风险
触发器内调用的存储过程如果包含事务控制(BEGIN...END 中有 START TRANSACTION)、显式加锁(SELECT ... FOR UPDATE)或跨表更新,会让锁持有时间变长、范围变广,进一步挤压并发窗口。
使用场景:比如订单表触发器调用 sp_update_inventory() 扣减库存,而该过程又去锁商品表和仓库表。
- 检查存储过程是否真的需要事务包裹——多数只读查询或单行更新无需
START TRANSACTION - 把
SELECT ... FOR UPDATE拆成先查后锁,或改用应用层重试逻辑,避免在触发器上下文中长期持锁 - 若过程必须更新多张表,确认这些表的更新顺序在所有业务路径中严格一致(例如总是先商品表再仓库表),否则易形成交叉锁
AFTER 触发器里做 INSERT INTO ... SELECT 易引发间隙锁冲突
AFTER INSERT 触发器中执行 INSERT INTO log_table SELECT * FROM main_table WHERE ... 这类语句,如果 WHERE 条件命中的是非唯一索引范围,InnoDB 会加间隙锁(gap lock),而主事务可能已持有该范围的插入意向锁(insert intention lock),二者互斥导致死锁。
性能影响:这类语句还容易拖慢主事务响应,尤其当 SELECT 扫描大量数据时。
- 避免在触发器中做任何带条件的全表扫描或范围扫描,哪怕目标表很小
- 改用基于主键或唯一索引的精确查找,例如
SELECT ... WHERE id = NEW.id,能大幅降低锁粒度 - 如果只是记录变更日志,优先用 MySQL 8.0+ 的
binlog+ 解析方案,或应用层写入审计表,绕过触发器
调试触发器死锁不能只看 SHOW ENGINE INNODB STATUS
该命令输出的 LATEST DETECTED DEADLOCK 区块虽然包含事务堆栈,但触发器内部的 SQL 不会单独显示为独立事务,而是附着在主事务下,容易误判锁源是业务 SQL 而非触发器逻辑。
容易踩的坑:开发者看到 “事务 A 锁了行 X,事务 B 锁了行 Y”,就去优化业务 SQL,却忽略了真正抢锁的是触发器里的 UPDATE audit_log SET count = count + 1 WHERE group_id = NEW.group_id。
- 临时禁用触发器(
ALTER TABLE t1 DISABLE TRIGGER tr_name)做对照测试,确认死锁是否消失 - 开启通用查询日志(
general_log = ON)并过滤含TRIGGER的行,定位具体哪条触发器语句在并发下出问题 - 用
INFORMATION_SCHEMA.TRIGGERS查触发器定义,重点关注是否有隐式全表扫描、子查询或函数调用(如NOW()、UUID()本身不危险,但嵌套在SELECT ... FOR UPDATE里会延长锁持有时间)
触发器的执行边界很模糊——它和主事务共享锁上下文,又没有独立事务 ID,这是最让人头疼的地方。处理时得把它当成主 SQL 的一部分来看,而不是一个“隔离模块”。


















