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