触发器本身不直接导致死锁,但因在主事务中隐式执行多条DML且加锁路径不可控,极易成为死锁关键环节;排查须从死锁图XML的executionStack确认触发器参与,比对holds/waiting表名及锁顺序,而非仅查触发器代码。

触发器本身不直接导致死锁,但一旦它在主事务中隐式执行多条 DML,又没控制好加锁路径,就极大概率成为死锁链上的关键一环——排查必须从死锁现场反推,而不是先翻触发器代码。
怎么看死锁图里有没有触发器参与
死锁发生后立刻执行 SHOW ENGINE INNODB STATUS\G(MySQL)或查 sys.dm_exec_requests + sys.dm_tran_locks(SQL Server),重点盯三处:
- 在
LATEST DETECTED DEADLOCK区块里找INSERT INTO orders后紧跟UPDATE audit_log这类连续操作痕迹——这就是触发器执行堆栈 - 看
TRANSACTION段的mysql tables in use 2, locked 2:数字大于 1 表明涉及多表,大概率有触发器介入 - 比对
HOLDS THE LOCK(S)和WAITING FOR THIS LOCK TO BE GRANTED中的表名,如果其中一把锁落在日志表、统计表这类“触发器专属目标表”上,基本坐实
为什么用 sys.dm_exec_trigger_stats 查不到线索
这个 DMV 只统计触发器执行次数,完全不记录锁行为。它告诉你“触发器跑过”,但不告诉你“它当时锁了哪几行、持有多久、走的是哪个索引”。真正要挖的,是触发器实际运行时的资源等待路径:
- 死锁图 XML 中的
executionStack里出现trg_after_update_order这类名字,才是触发器卷入的铁证 - 如果触发器里调用了存储过程,
executionStack可能只显示外层,得结合sys.dm_exec_sql_text手动反查sql_handle对应的完整语句 -
SET STATISTICS XML ON开启后跑一次触发器对应的操作,看执行计划里有没有Key Lookup或Table Scan——这些意味着锁范围被意外放大
触发器内哪些写法会悄悄拉长锁时间
触发器共享主事务上下文,它做的任何事都会延长主事务的锁持有时间。以下写法看似无害,实则高危:
- 触发器里调用远程 HTTP 接口(如
sp_OACreate):网络延迟直接拖住整个事务 - 执行大体积日志写入,比如
INSERT INTO huge_audit_log:I/O 慢 + 索引维护开销大 - 嵌套调用另一个含 DML 的存储过程,且该过程没设
SET XACT_ABORT ON:异常时事务状态残留,锁不释放 - 误用
IF @@TRANCOUNT = 0 BEGIN BEGIN TRAN ... END:在触发器里@@TRANCOUNT永远 ≥1,这个判断永远不成立,但开发者以为“安全”就放行了
怎么验证触发器锁顺序是否与其他事务冲突
死锁本质是加锁顺序不一致。两个事务都操作 orders 和 inventory 表,但 A 先锁 orders 再锁 inventory,B 反过来,就闭环了。触发器会让这个顺序更隐蔽:
- 主 SQL 走
PRIMARY KEY更新订单,触发器却用WHERE status = 'pending'去更新日志表 → 走非唯一索引,触发间隙锁,锁范围错位 - 触发器里子查询
SELECT balance FROM accounts WHERE user_id = NEW.user_id,InnoDB 可能提前对accounts加 gap lock,而主事务又在更新同一张表其他行 → 等待环形成 - 用
SELECT ... FOR UPDATE预锁触发器要操作的行(如SELECT id FROM t_log WHERE order_id = NEW.order_id FOR UPDATE),确保锁顺序与触发器内一致
真正难处理的不是触发器本身,而是它把锁行为藏在主事务背后,让加锁路径不可见、不可控。排查时别只盯着触发器代码,要回到死锁图里看 executionStack、看锁类型、看表访问顺序——那里才有真实发生的证据。

















