SQL Server触发器易引发死锁,主因是DML未走索引导致锁升级、ABBA锁序、递归更新及事务延长;应确保每条DML走主键/唯一索引、禁用范围条件、拦截嵌套、剥离同步逻辑、避免外部调用,并在应用层捕获1205重试。

触发器内所有DML必须走主键或唯一索引
触发器里一条没走索引的 UPDATE,在 SQL Server 中可能升级为页锁甚至表锁;而另一事务按相反顺序操作,ABBA 锁序瞬间形成死锁。你写的业务语句可能走 PRIMARY KEY,但触发器里一句 UPDATE log_table SET status = 'done' WHERE order_id = @id 若 order_id 上只有非聚集索引且未覆盖,就极易错位。
实操建议:
- 对触发器中每条
UPDATE/INSERT/DELETE,用SET STATISTICS XML ON跑一次真实参数,确认执行计划中type是const或seek,不是scan或lookup - 禁用
WHERE status = 'pending'这类范围条件——它会引发锁升级,统一改用主键或唯一键更新 - 被高频更新的目标表(如日志表),去掉大字段
INCLUDE,把varchar(max)改成varchar(400),缩小索引宽度可显著降低锁冲突概率
别在触发器里更新同一张表
看似安全的“自己改自己”,在 SQL Server 中极易触发递归等待:主事务已对某行持 U 锁,触发器再发一条 UPDATE 同一行,就是二次申请锁。更糟的是,若该触发器是 AFTER 类型,还可能触发新一轮自身调用,直到达到嵌套深度上限 32 或卡死在 LCK_M_U 等待上。
实操建议:
- 用
IF TRIGGER_NESTLEVEL(OBJECT_ID(N'dbo.trg_orders')) > 1 RETURN在触发器开头硬性拦截嵌套 - 需要赋值修改时,优先用
INSTEAD OF触发器 + 显式INSERT/UPDATE,避开隐式再触发 - 真要同步更新多字段(如订单状态+子表计数),剥离出触发器,交由应用层单事务完成,或写入轻量变更表由后台作业批量聚合
避免触发器拉长事务生命周期
触发器共享主事务上下文,它做的任何事都会延长锁持有时间。一个本该毫秒级完成的 INSERT,若触发器里调了远程 HTTP 接口、写了大体积日志、或嵌套执行含 DML 的存储过程,就会把锁从毫秒拖到秒级——并发一上来,死锁只是时间问题。
实操建议:
- 触发器内严禁调用
sp_OACreate、xp_cmdshell或任何外部资源访问逻辑 - 审计类日志写入,改用
INSERT INTO log_buffer (order_id, event_type) VALUES (@id, 'created'),再由定时任务消费,而非直接写audit_log - 检查是否误用了
IF @@TRANCOUNT = 0 BEGIN BEGIN TRAN END—— 在触发器里@@TRANCOUNT永远 ≥ 1,这个判断永远不成立,但开发者常以为“加了保护”就放行了
重试逻辑必须放在调用方,不能塞进触发器
SQL Server 对死锁错误 1205 的处理机制很明确:它不进 TRY/CATCH,而是直接终止当前执行上下文。你在触发器或存储过程里写的 BEGIN TRY ... END TRY BEGIN CATCH ... END CATCH 对 1205 完全无效。试图在 CATCH 里写 WAITFOR DELAY 或递归调用自己,根本不会执行到那行。
实操建议:
- 真正能捕获 1205 的位置,是执行该语句的外部批处理(比如 SSMS 查询窗口、SQL Agent 作业步骤、或应用拼接的完整 T-SQL)
- 用
WHILE @retry 包裹 <code>EXEC your_proc - 更推荐由应用层(C#、Java、Python)捕获
SqlException.Number == 1205后重发请求——变量状态不丢失,逻辑可控,且避免在数据库侧堆积重试压力
触发器的危险不在它存在,而在它的锁行为完全不可见:你很难从应用日志里看出某次 INSERT 背后悄悄锁了三张表。排查时别只盯错误码 1205,得进死锁图 XML 的 executionStack 里找触发器名;优化时也别幻想靠改隔离级别,得一条一条核对触发器内每条 DML 的执行计划和索引路径。










