存储过程内无法真正重试网络瞬时故障,因会话终止导致上下文丢失;有效重试必须由外部调用方控制,包裹exec的外层批处理中实现事务全生命周期管理,并仅对可幂等逻辑重试。

SQL Server 存储过程内部无法对网络瞬时故障(如连接中断、超时、1205死锁)做真正意义上的“重试”——因为错误发生时执行上下文已终止,局部变量、事务状态、游标全部丢失。所谓“优雅重试”,必须由外部调用方控制,且仅适用于可幂等的事务逻辑。
为什么存储过程里写WHILE + TRY CATCH对网络瞬时错误无效
常见误解是把重试逻辑塞进存储过程,比如声明@retry计数器、在CATCH里GOTO start。这根本行不通:
- 网络中断或严重超时(如错误 10053、10060、1205)会直接终止会话,整个批处理退出,
CATCH块不会执行 -
@retry变量在会话终止后不复存在,下一次调用是全新上下文,计数器永远是初始值 - 试图在
CATCH里重新BEGIN TRANSACTION会导致The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION报错,因为事务早已被引擎强制清理 - 死锁错误 1205 从不进入
CATCH,它比 TRY/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中只做两件事:IF ERROR_NUMBER() IN (1205, 1222, 1204, 701, 1213)判断是否值得重试;其余错误(如主键冲突、类型转换失败)应立即THROW - 重试前不要修改输入参数——比如传入
@order_id,重试时不能自增或加时间戳,否则业务语义错乱
应用层重试比T-SQL层更可靠、更可观测
比起在数据库里硬塞重试逻辑,由 C#、Java 或 Python 捕获异常后重发请求,才是生产环境首选:
- 能记录每次重试的原始参数、耗时、线程 ID、调用栈,便于定位竞争热点
- 可实现退避策略:第一次等 100ms,第二次 300ms,第三次 900ms(指数退避),避免雪崩
- 可结合分布式锁或幂等 key(如
idempotency_key = SHA2(CONCAT(@user_id, @timestamp)))防止重复提交 - SQL 层无法区分“网络断开”和“业务拒绝”,而应用层可根据
SqlException.Number和Exception.InnerException?.Message做精细判断
最容易被忽略的一点:所谓“事务重试”,从来不是让同一段 SQL 多次执行,而是让同一笔业务逻辑,在干净的会话、新的事务、带延迟的时机下,重新走一遍。任何试图在存储过程内部“续上”上次失败的操作,都是对 SQL Server 执行模型的根本误读。










