SQL Server触发器内无法捕获死锁错误(1205),因其在语句执行中途被终止,TRY...CATCH无效;死锁重试必须由外部批处理或应用层实现,并通过WHILE循环+TRY...CATCH集中控制。

SQL Server触发器里不能直接捕获死锁异常
触发器执行期间如果发生死锁,SQL Server 会直接终止当前事务并抛出 1205 错误(死锁牺牲品),而触发器内部无法用 TRY...CATCH 捕获该错误——因为死锁检测发生在语句执行中途,CATCH 块根本不会被进入。你写的任何 TRY...CATCH 在触发器里对 1205 都是无效的。
常见错误现象:
– 在 AFTER INSERT 触发器中加了 TRY...CATCH,仍看到应用层报错 Transaction (Process ID XX) was deadlocked... ROLLBACK executed.
– 尝试在触发器内 WAITFOR DELAY 后重试,但触发器已退出,重试逻辑根本不执行
- 死锁处理必须由调用方(即发起
INSERT/UPDATE/DELETE的外部批处理或应用代码)负责 - 触发器只适合做轻量、确定性高的数据校验或派生字段更新,不适合含 I/O、远程调用、复杂循环等易阻塞操作
- 若触发器内执行了
INSERT INTO other_db.dbo.table或调用EXEC xp_cmdshell,死锁概率会显著上升
真正的死锁重试必须放在外部 T-SQL 批处理中
唯一可控的位置是触发器的“上游”:即执行原始 DML 的那个批次。你需要把 DML 包裹进带重试逻辑的 WHILE 循环,并用 TRY...CATCH 捕获 1205。
典型结构如下:
DECLARE @retry INT = 0, @maxRetry INT = 3; WHILE @retry
-
WAITFOR DELAY时间建议从00:00:00.05起步,避免重试风暴;超过 3 次就应放弃 - 不要在
CATCH里再开新事务——ROLLBACK后事务已结束,再次BEGIN TRANSACTION才合法 - 应用层调用时,也需设置命令超时(如 ADO.NET 的
CommandTimeout)大于最大重试总耗时
触发器自身如何降低死锁概率
减少触发器成为死锁节点的概率,比在它里面搞重试更有效。核心原则是:越快完成、越少锁资源、越少跨表操作越好。
- 避免在触发器中更新其他大表,尤其不要更新被高频并发修改的表(如
Inventory、BalanceLog) - 用
INSERTED和DELETED表做集合操作,别写游标遍历 - 如果必须异步通知,改用
Service Broker或写入队列表(INSERT INTO QueueTable),由后台作业消费 - 检查索引:确保触发器里
WHERE条件能走索引,避免全表扫描锁升级
例如,这个低效写法极易引发死锁:
-- ❌ 危险:无索引条件 + 全表扫描 + 更新另一张热表 UPDATE Accounts SET Balance = Balance + i.Amount FROM Accounts a INNER JOIN INSERTED i ON a.CustomerID = i.CustomerID WHERE a.Status = 'Active'; -- 假设 Status 列没索引
替代方案:用存储过程封装完整逻辑,绕过触发器
把原本分散在触发器和应用中的逻辑,统一收口到一个存储过程中,由它控制事务边界、重试和错误分类,是最干净的解法。
- 应用不再直接
INSERT INTO Orders,而是调用EXEC usp_InsertOrder @CustomerID=123, @Amount=99.99 - 存储过程内显式
BEGIN TRAN→ DML →COMMIT,重试逻辑集中、可测、可监控 - 触发器彻底移除,消除隐式执行带来的不可控性
- 配合
WITH (UPDLOCK, HOLDLOCK)提前锁定关键行,进一步降低冲突
触发器看似自动,实则让事务行为变得隐蔽且难以调试。真正高并发场景下,显式控制永远优于隐式依赖。
死锁重试不是加个 TRY 就能解决的事,关键在于谁持有事务、谁决定重试、谁承担延迟成本——这些责任必须落在可控的入口点上,而不是塞进一个无法捕获错误的触发器里。










