try catch 在存储过程中无法捕获 1205 死锁错误,因为死锁发生时事务被强制终止,控制权不进入 catch 块;仅最外层批处理可捕获并重试,且须包裹整个事务、显式判断错误号、加退避延迟并限制重试次数。

不能在存储过程中用 TRY CATCH 捕获 1205 错误来实现自动重试——因为死锁发生时,当前批处理已被强制终止,CATCH 块根本不会执行。
为什么存储过程里的 TRY CATCH 对 1205 无效
SQL Server 在检测到死锁后,会立即将其中一个事务标记为“牺牲品”,直接回滚并中断其整个执行上下文。此时控制权不会回到你写的 CATCH 块,哪怕它就在 UPDATE 后面紧挨着。常见错误现象包括:
- 在存储过程开头写
BEGIN TRY ... END TRY BEGIN CATCH ... END CATCH,但应用层仍直接收到Transaction (Process ID XX) was deadlocked - 试图在
CATCH里调用WAITFOR DELAY+ 再次执行逻辑,结果语句压根没运行 - 把重试逻辑塞进触发器或嵌套存储过程,同样失效——死锁发生在语句级,不是过程级
真正能捕获并重试 1205 的位置:调用方批处理
只有发起 DML 的最外层 T-SQL 批处理(比如 SSMS 中手动执行的脚本、SQLCMD 脚本、或由应用拼接后发过来的完整语句块)才能用 TRY CATCH 捕获 1205 并控制重试。关键点:
- 重试必须包裹整个事务,而不是单条语句:
BEGIN TRANSACTION; ... COMMIT;要在TRY内部 -
ERROR_NUMBER()必须显式判断是否等于1205,其他错误(如约束冲突、超时)不应重试 -
WAITFOR DELAY '00:00:00.05'是必要的退避,避免瞬间重试引发“重试风暴” - 超过 3 次就该
THROW原错误,防止无限循环掩盖真实问题
典型结构:
DECLARE @retry INT = 0, @maxRetry INT = 3; WHILE @retry <h3>如果必须从应用层调用存储过程,该怎么办</h3><p>这时重试责任完全转移到应用代码。数据库层只负责抛出清晰错误,应用需做三件事:</p>
- 捕获 SQLSTATE
40001或错误号1205(不同驱动写法不同,如 .NET 的SqlException.Number == 1205) - 在重试前加随机抖动延迟(例如
Thread.Sleep(50 + new Random().Next(50))),比固定等待更抗并发 - 限制重试次数,并记录每次失败的输入参数,方便后续分析是否是某类数据高频触发死锁
注意:不要在存储过程中加 sp_getapplock 或 UPDLOCK 来“防死锁”——这只会把死锁变成阻塞,且掩盖了资源访问顺序不一致这个根本问题。
死锁重试成功 ≠ 问题解决
反复靠重试扛住 1205,说明底层设计存在隐患。最容易被忽略的是:
- 两个存储过程更新同一组表,但顺序相反(如 A 先改
Orders再改Inventory,B 正好反过来) - 查询中用了
SELECT ... FROM TableA JOIN TableB却没走覆盖索引,导致锁住大量无关行 - 把“先查再更”的逻辑留在存储过程里,而没改成
MERGE或带HOLDLOCK的原子读取
重试只是容错手段,不是替代锁序统一、索引优化和事务瘦身的方案。










