sql触发器导致死锁的核心原因是其隐式延长主事务锁生命周期,引发abba锁序、未索引导致全表锁、伪表逐行处理放大竞争。

触发器隐式延长主事务锁生命周期
触发器不开启新事务,它完全运行在主事务上下文中。主事务没提交,触发器里所有 UPDATE、INSERT、SELECT FOR UPDATE 产生的锁就一直挂着。你执行一条 UPDATE orders,背后可能已悄悄给 user_points、audit_log、stats_summary 同时加了 X 锁——这些锁要等整个事务结束才释放。
常见错误现象:Deadlock found when trying to get lock 报错中堆栈显示的 SQL 往往是触发器内部那句 UPDATE audit_log SET ... WHERE order_id = NEW.id,但应用日志只记了主表操作,排查时容易漏掉这一环。
多跳触发链天然形成 AB-BA 锁序
当触发器更新另一张表,而这张表上又有自己的触发器(比如 orders → user_points → points_audit),锁路径就从单跳变成多跳。InnoDB 和 SQL Server 都不会为触发器重排加锁顺序,只按实际执行流加锁。
多个并发请求进入后,极易出现:
- 线程 A 先锁
user_points再等points_audit - 线程 B 先锁
points_audit再等user_points
这就是典型的 ABBA 死锁链。高发场景包括:订单插入触发积分更新,积分表变更又触发审计写入,同时还有后台任务直接更新积分表——三股力量交叉争夺同一行资源。
触发器内 DML 缺索引导致锁范围爆炸
触发器里一句看似普通的 UPDATE config SET value = 'on' WHERE module = 'payment',如果 module 字段没索引,InnoDB 就得全表扫描聚簇索引,并对每行都加 X 锁。哪怕最终只改一行,也等于锁住了整张表的活跃数据段。
检查必须实操:
- 对触发器中每条 DML,用真实参数模拟执行
EXPLAIN FORMAT=TRADITIONAL - 确认
type不是ALL或index - 确认
key字段非NULL,且rows估算值接近实际影响行数 - 若发现
Using where; Using index condition缺失,立刻补索引
SQL Server 中伪表误用放大锁竞争
把 inserted 当成单行变量处理,是高频死锁源头之一。例如:
SELECT @id = id FROM inserted; -- ❌ 只取第一行,其余丢弃 UPDATE t2 SET status = 'done' WHERE id = @id;
这会导致:10 万行批量更新,触发器被调用 10 万次,每次只处理一行,锁反复申请释放;正确做法是集合运算:
UPDATE t2 SET status = 'done' FROM t2 INNER JOIN inserted ON t2.id = inserted.id;
真正棘手的是:这类问题在单行测试时完全不暴露,只有批量写入压测或上线后高并发时才突然爆发,且死锁日志里根本看不到原始业务 SQL,只看到触发器内部那条孤立的 UPDATE —— 容易误判为“底层数据库问题”。











