死锁是多个并发事务因循环等待资源而卡死,sql server自动选牺牲者报错1205;需通过错误日志或dbcc traceon(1222,-1)获取xml死锁图,分析process的inputbuf和resource-list定位循环依赖;set lock_timeout对死锁无效,真正解决须从索引优化、统一访问顺序、拆分事务三方面入手。

死锁不是“调用存储过程出错了”,而是多个并发事务在争夺资源时卡死,SQL Server 自动选一个牺牲者并报错 1205。它不靠重试、不靠加 SET LOCK_TIMEOUT 解决,必须从锁行为本身入手。
怎么看死锁图里谁在卡谁
收到 Deadlock encountered 后,第一件事是查 SQL Server 错误日志或启用 DBCC TRACEON(1222, -1) 获取 XML 死锁图。重点盯两个部分:
-
process节点里的inputbuf:直接告诉你每个会话最后执行的那条语句,比如UPDATE Orders SET status = 'shipped' WHERE order_id = 123 -
resource-list里的资源类型:KEY表示某一行被锁,PAGE是数据页,OBJECT是整张表——锁粒度越大越危险 - 比对两个
process的inputbuf和它们等待的resource,就能还原循环链:A 在等 B 锁的行,B 又在等 A 锁的行
为什么加 SET LOCK_TIMEOUT 完全没用
很多人在存储过程开头写 SET LOCK_TIMEOUT 5000,以为能“防死锁”,结果照样报 1205。原因很实在:
-
LOCK_TIMEOUT只控制单条语句阻塞等待上限(超时抛1204),而死锁是引擎在 1–5 毫秒内主动检测并终止事务(报1205),根本没机会等到超时 - 更危险的是把它和重试逻辑混用:第一次死锁回滚后,数据状态已变(比如某行被其他事务改了),重试反而引发新冲突或业务错乱
- 它只适用于明确知道可能被别人长期锁住的场景,比如带
WITH (TABLOCK)的 SELECT;对死锁毫无意义
真正有效的三类动作:索引、顺序、拆分
死锁归根结底是锁范围太大 + 锁持有太久 + 访问顺序不一致。解决必须落在具体操作上:
-
索引优化:让
UPDATE精准锁行,而不是扫全表。给WHERE条件字段建联合索引,顺序按AND出现顺序排;删掉无用的INCLUDE列(尤其是大字段),避免锁升级为页锁 -
统一访问顺序:多表操作时,所有存储过程必须严格按同一顺序访问表(如总是先
Customers→ 再Orders→ 最后OrderItems);注意触发器、外键级联、IN (SELECT ...)子查询也会悄悄引入额外表,必须纳入顺序约束 -
缩短事务时间:把一次更新 1000 行的大事务,改成
WHILE循环 +TOP (50)分片 + 显式COMMIT;把 RPC 调用、日志写入、JSON 序列化等非 DB 操作全部移到COMMIT之后
最容易被忽略的点是:同一张表多行更新时,WHERE id IN (9,1,5) 和 WHERE id IN (1,5,9) 在锁获取顺序上完全不同——哪怕只操作一张表,隐式顺序不一致也会引发死锁。










