存储过程内无法实现真正有效的自动重试,因网络中断(10053)、死锁(1205)、锁超时(1222)等错误会直接终止会话,CATCH不执行、@retry变量丢失、事务状态不可恢复;唯一有效重试位置是调用方外层批处理,须完整包裹BEGIN TRAN/EXEC/COMMIT,并仅对可幂等操作按ERROR_NUMBER()精准重试。

SQL 存储过程本身无法实现真正有效的自动重试——重试必须由调用方控制,且仅对可幂等操作有意义。
为什么存储过程里写 WHILE + TRY CATCH 重试总是失败
常见做法是在存储过程中声明 @retry 变量、用 GOTO 或 WHILE 循环包裹 TRY CATCH,但这在真实网络或锁超时场景下完全无效:
- 错误如
1205(死锁)、1222(锁超时)、10053(连接中断)会直接终止会话,CATCH块根本不会执行 -
@retry是局部变量,会话一断就丢失,下次 EXEC 是全新上下文,计数器永远是初始值 - 事务状态不可恢复:CATCH 中尝试
ROLLBACK或BEGIN TRANSACTION会报错The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION - SQL Server 对
1205的处理优先级高于 TRY/CATCH,它不走 CATCH 流程
真正可控的重试位置:外层批处理(SSMS / SQL Agent / 应用拼接语句)
唯一能完整管理事务生命周期和重试逻辑的地方,是包裹 EXEC YourProc 的最外层 T-SQL 批处理:
- 用
WHILE @retry 控制总次数,每次失败后加 <code>WAITFOR DELAY '00:00:00.1'(别用0.01,易引发重试风暴) -
BEGIN TRY内必须包含BEGIN TRANSACTION、EXEC YourProc、COMMIT TRANSACTION三者,缺一不可 -
CATCH中只判断ERROR_NUMBER():仅当为1205、1222、1204、701等可重试错误才继续循环;其余如主键冲突、类型转换失败应立即THROW - 重试时禁止修改输入参数(如传入
@order_id,不能自增或加时间戳),否则业务语义错乱
应用层重试比 SQL 层更可靠、更可观测
生产环境首选由 C#、Java 或 Python 实现重试,而非在数据库里硬塞逻辑:
- 能记录每次重试的原始参数、耗时、线程 ID、调用栈,便于定位竞争热点
- 可实现指数退避:第一次等
100ms,第二次300ms,第三次900ms,避免雪崩 - 可结合幂等 key(如
idempotency_key = SHA2(CONCAT(@user_id, @timestamp)))防止重复提交 - 能区分
SqlException.Number和Exception.InnerException?.Message,精准识别是网络断开还是业务拒绝
SET LOCK_TIMEOUT 是辅助手段,不是重试开关
SET LOCK_TIMEOUT 只控制锁等待上限,不影响 CPU 密集型操作,也不能替代重试逻辑:
- 设为
5000表示最多等 5 秒,超时抛出错误号1205(注意:这不是死锁错误号,死锁是1204) - 必须在存储过程开头显式设置,作用域仅限当前会话,不继承自调用方
- 对
SELECT有效,但对INSERT/UPDATE/DELETE中的锁争用更关键;对大表排序、递归 CTE 完全无效 - 值设为
0表示不等待,冲突立即失败;设为-1是默认无限等待,生产环境不建议
最容易被忽略的一点:所谓“事务重试”,从来不是让同一段 SQL 多次执行,而是让该事务逻辑在幂等前提下,由外部可控边界重新发起一次完整生命周期。只要还在触发器或存储过程内部写重试,就等于在失效的抽象上叠加复杂度。

















