SET LOCK_TIMEOUT不能直接救急,但它是SQL Server中唯一能在语句级控制锁等待时长的原生机制,需配合TRY...CATCH与手动重试逻辑使用,且须区分应用层超时与锁超时。

存储过程执行卡死时,SET LOCK_TIMEOUT 能不能救急
不能直接“救急”,但它是唯一能在语句级控制阻塞等待时长的原生机制。SQL Server 不支持在存储过程中全局设置查询超时(那是应用层或连接字符串的事),SET LOCK_TIMEOUT 只影响后续的锁等待行为,不终止已运行的计算逻辑。
常见错误现象:sp_who2 显示状态为 sleeping 但会话长期不返回,实际是卡在锁等待里;或者日志里反复出现 Timeout expired. The timeout period elapsed prior to completion of the operation,但源头不是网络或连接池——而是某条 UPDATE 被另一事务锁住太久。
-
SET LOCK_TIMEOUT 5000表示最多等 5 秒,超时抛出错误号1205(注意:不是死锁错误号,死锁是1204) - 必须在存储过程开头显式设置,作用域仅限当前会话,不会继承自调用方
- 对
SELECT有效,但对INSERT/UPDATE/DELETE中的锁争用更关键;对 CPU 密集型操作(如大表排序、递归 CTE)完全无效 - 值设为
-1是默认行为(无限等待),设为0则完全不等待,冲突立即失败
重试逻辑必须由存储过程自己实现,TRY...CATCH 是基础但不够
SQL Server 没有内置重试关键字,所有重试都要靠 WHILE + TRY...CATCH 手写循环。只靠 CATCH 捕获错误并 RETURN,等于放弃重试机会。
使用场景:上游系统无法控制重试(比如 legacy 客户端只发一次调用),或业务要求“尽力而为”(如日志落库、状态同步类操作)。
- 必须在
CATCH块中检查ERROR_NUMBER(),只对1205(死锁)、1222(锁超时)、1204(死锁牺牲品)这类可重试错误才执行重试 - 重试前加
WAITFOR DELAY '00:00:00.1',避免瞬间重试加剧锁竞争;延迟建议从 100ms 起步,逐次倍增(最多 3 次) - 用局部变量记录重试次数,防止无限循环;超过阈值应
THROW原始错误,让调用方兜底 - 注意事务嵌套:如果外层已有事务,
ROLLBACK会回滚整个事务,重试前需确保事务状态可恢复(通常建议重试逻辑独占事务)
应用层超时和存储过程内超时要分清楚,别互相覆盖
连接字符串里的 Connection Timeout=30 控制的是连上 SQL Server 的耗时,和存储过程执行无关;CommandTimeout(如 ADO.NET 的 SqlCommand.CommandTimeout)才是真正的执行超时开关,它触发的是客户端强制断开,SQL Server 端会收到 ERROR 0 并回滚当前批处理。
容易踩的坑:在存储过程中设了 SET LOCK_TIMEOUT 3000,但应用层 CommandTimeout 设成 10 秒,结果锁等 3 秒失败后重试两次,每次又卡 3 秒,第 3 次还没跑完就被客户端砍掉——此时你看到的错误是 Execution Timeout Expired,而不是你预设的锁超时错误。
- 存储过程内超时(
LOCK_TIMEOUT)解决的是“等不等得及”,应用层超时解决的是“给不给足够时间” - 两者应配合:应用层超时 ≥ 单次最大可能耗时 × 重试次数 + 预留缓冲(例如单次最长 8 秒,重试 3 次,设
CommandTimeout=30) - 不要在存储过程中用
WAITFOR模拟超时,它不释放资源,还拖慢整个会话
真正稳定的重试需要识别“可重试”和“不可重试”错误类型
不是所有报错都能重试。盲目重试 CONVERT 失败、主键冲突、权限不足这类错误,只会让问题更快暴露为高频告警。
性能影响很实际:每次重试都重新走一遍执行计划、重新申请锁、重新读数据页。若底层是高并发更新热点行,重试反而放大锁争用。
- 安全重试的错误号:
1204(死锁牺牲品)、1205(死锁)、1222(锁超时)、1221(锁升级失败) - 禁止重试的错误号:
2627(唯一键冲突)、547(外键约束)、245(类型转换失败)、208(对象名无效) - 建议把错误分类逻辑封装成标量函数,例如
dbo.IsRetryableError(@errnum),避免每个存储过程重复写判断 - 重试日志必须记清楚:原始错误号、重试次数、最终是否成功——否则出问题时根本分不清是第几次失败
最麻烦的其实是那些没报错但“不动”的情况:比如某个 UPDATE 匹配了 100 万行,但 WHERE 条件没走索引,实际在扫全表。这种时候 LOCK_TIMEOUT 和重试都无能为力,得靠执行计划分析和索引优化——那已经是另一层稳定性问题了。










