SQL触发器本身不直接导致死锁,但会将单条SQL隐式扩展为多步DML并塞入同一事务,造成锁不释放、顺序不可控、范围易爆炸,从而大幅提高死锁概率;典型如AFTER触发器中UPDATE同表引发循环等待,应改用BEFORE中SET NEW赋值或移至应用层异步处理。

SQL触发器本身不直接导致死锁,但它会把单条业务 SQL 隐式扩展成多步 DML 操作,并全部塞进同一个事务里——锁不释放、顺序不可控、范围易爆炸,死锁就从“可能”变成“大概率”。
触发器内 UPDATE 同一张表为什么直接触发死锁
看似安全的 UPDATE t1 SET status = 'done' WHERE id = NEW.id,在 InnoDB 中极易形成循环等待:主事务已对 NEW.id 所在行持 X 锁,触发器再发一条相同条件的 UPDATE,等于二次申请同一行锁。
- 正确做法是在
BEFORE INSERT或BEFORE UPDATE触发器中用SET NEW.status = 'done'直接赋值,不走额外 SQL - 若需更新多个字段(如订单状态 + 子表计数),必须剥离出触发器,交由应用层异步完成
- 真要在数据库侧维护统计值,可用
INSERT INTO summary_log写日志表(引擎选BLACKHOLE),再由定时任务批量聚合
死锁日志里怎么一眼识别是触发器惹的祸
死锁发生后立刻执行 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的堆栈顺序——这就是触发器执行痕迹
触发器里哪些 DML 操作必须立刻砍掉
以下行为在高并发下几乎等于主动制造死锁热点:
- 跨业务主表的
UPDATE(例如从orders触发器去改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,得进死锁图 XML 里的 executionStack 找触发器名;优化时也别幻想靠改隔离级别解决,关键在让每条触发逻辑的锁粒度、顺序、索引都确定可控。

















