SET LOCK_TIMEOUT 对死锁完全无效,它只控制阻塞等待超时(报错1204),而死锁由引擎毫秒级检测并主动终止事务(报错1205);真正有效的措施是索引优化、缩短事务持锁时间及显式排序更新顺序。

SET LOCK_TIMEOUT 对死锁完全无效,它只管阻塞等待超时(报错 1204),而死锁是引擎毫秒级检测并主动终止事务(报错 1205)——两者根本不是一回事。
为什么在存储过程中加 SET LOCK_TIMEOUT 5000 没用
很多人在存储过程开头写 SET LOCK_TIMEOUT 5000,以为能“防死锁”,结果上线后照样频繁收到 Deadlock encountered。原因很直接:
- 死锁发生时,SQL Server 的死锁监视器通常在 1–5 毫秒内就完成检测、选 Victim、回滚事务,
LOCK_TIMEOUT根本没机会触发 - 它只影响单个语句遇到阻塞时的等待上限,比如
SELECT ... WITH (TABLOCK)被别人锁住表,等 5 秒就抛1204;但死锁里根本没“等”,是立即中断 - 更危险的是把它和重试逻辑混用:第一次死锁回滚后,重试时数据状态已变(比如某行被其他事务改了),反而引发新冲突或逻辑错乱
真正起效的索引优化点:让 UPDATE 只锁该锁的行
同一个 UPDATE 语句,在不同索引下锁的范围可能天差地别。没索引走全表扫描 → 锁整页甚至整表;有覆盖索引 → 精准锁定目标行。实测中,以下改动直接让死锁归零:
- 给
WHERE条件字段加索引,尤其是组合条件要建联合索引,顺序按查询中AND出现顺序排 - 把
INCLUDE列从大字段(如varchar(max))换成小字段(如varchar(200)),避免 SQL Server 因行太宽而退化为页锁 - 删除无用的
INCLUDE列(比如只用于SELECT输出但UPDATE不涉及),减少索引维护开销与锁升级概率 - 用
EXPLAIN(MySQL)或执行计划(SQL Server)确认是否真走索引查找(Seek),而非扫描(Scan)
缩短事务持锁时间:拆、排、移
锁持有时间 ≈ 事务执行时间。哪怕逻辑再正确,只要事务拖得久,死锁风险就指数上升。关键动作就三类:
-
拆:把一次更新 1000 行的事务,改成
WHILE循环 + 每次 50 行 + 显式COMMIT;SQL Server 中可用TOP (50)配合NOT IN (SELECT TOP ...)分片 -
排:对批量操作,强制按主键升序排序后再执行 —— 比如
UPDATE t SET x=1 WHERE id IN (9,1,5)改成WHERE id IN (1,5,9),避免不同事务因顺序不一致形成锁循环 -
移:把事务内非数据库操作全部移出去 —— RPC 调用、日志写入、JSON 序列化、甚至
GETDATE()这种函数调用,都放到COMMIT之后再做
最容易被忽略的细节:同一张表多行更新的隐式顺序
你以为只操作一张表就不会有锁序问题?错。当两个存储过程都执行 UPDATE orders SET status = 'shipped' WHERE id IN (@id_list),但传入的 @id_list 是无序数组(比如应用层用 HashSet 生成),SQL Server 内部处理顺序由执行计划决定,可能这次是 1→5→9,下次变成 9→1→5 —— 这就足以和另一个事务构成循环等待。解决方案很简单:在存储过程里显式排序,例如用临时表 + ORDER BY id 插入,再 JOIN 更新。这个细节不查死锁图根本看不到,但一加就稳。

















