SQL Server存储过程本身不引发死锁,死锁源于并发事务循环等待资源;关键在事务设计、索引合理性与表访问顺序一致性。

SQL Server 存储过程本身不会“引发”死锁,死锁是多个并发事务在争夺资源时形成的循环等待——只要存储过程里有写操作(UPDATE、DELETE、带 UPDLOCK 的 SELECT)且并发执行,就可能卷入死锁。真正的问题出在事务设计、索引缺失和访问顺序混乱上。
怎么看死锁图里谁在卡谁
收到错误 1205 后,别急着改代码,先定位真实冲突点。SQL Server 默认不记录死锁,得主动启用捕获:
- 推荐用扩展事件
system_health(开销小、无需重启),或临时启用DBCC TRACEON(1222, -1) - 查到的 XML 死锁图中,重点比对两个
process节点:inputbuf显示最后执行语句(如UPDATE Orders SET status = 'shipped' WHERE order_id = 123),resource-list显示锁类型(KEY行锁、PAGE页锁、OBJECT表锁) - 如果
inputbuf只显示存储过程名,用sys.dm_exec_sql_text查完整语句,尤其注意动态 SQL 拼接位置 - 别只盯着
deadlock victim:被杀掉的那个事务,往往只是持锁时间短、代价低;真正长期占着资源不放的,才是根因
为什么加了索引反而死锁更多了
索引不是银弹,建错会放大锁争用。常见反模式:
- 新建非聚集索引没覆盖
SELECT所有字段,触发Key Lookup,导致同时锁住非聚集索引页 + 聚集索引键,锁数量翻倍 -
WHERE条件用了函数(如WHERE YEAR(CreateTime) = 2024)或隐式转换,索引失效,全表扫描 → 行锁升级为页锁甚至表锁 - 复合索引列顺序和查询条件不匹配:建了
IX_A_B_C,但查询只用B = ?,无法跳过前导列,扫描范围大 - 统计信息陈旧,优化器选错执行计划,本该走索引却走了
Clustered Index Scan
验证方法:对死锁语句运行 SET STATISTICS XML ON,看执行计划里有没有 Index Scan、Key Lookup 或 Sort(可能触发排序溢出并加锁)。
存储过程中哪些写法最容易踩坑
以下模式在高并发下高频触发死锁,排查时可直接搜存储过程体:
-
UPDATE和SELECT表访问顺序不一致:A 过程先改Customers再查Orders,B 过程反过来,立刻形成循环等待 - 隐式事务开着:
SET IMPLICIT_TRANSACTIONS ON导致单条语句也开启事务,不显式COMMIT就一直持锁 - 循环内分批
UPDATE(如TOP (100))但每次范围不同,锁范围重叠 + 锁升级风险高 - 调用外部接口(
xp_cmdshell、链接服务器)卡住,事务挂起但锁未释放 - 触发器或外键级联操作悄悄引入额外表,打破你自以为的“统一访问顺序”
SET LOCK_TIMEOUT 对死锁完全无效——它只控制单条语句等待上限(超时报 1204),而死锁是引擎在 1–5 毫秒内主动检测并终止(报 1205)。加了反而容易掩盖真实问题。
真正有效的三类动作:索引、顺序、拆分
死锁归根结底是锁粒度太大 + 锁持有太久 + 访问顺序不一致。必须落到具体操作上:
-
索引优化:给
WHERE条件字段建联合索引,顺序按AND出现顺序排;删掉无用的INCLUDE列(尤其是大字段),避免锁升级 -
统一访问顺序:所有存储过程严格按同一顺序访问表(如总是
Customers → Orders → OrderItems);把触发器、外键、IN (SELECT ...)也纳入顺序约束 -
拆分事务:把一次更新 1000 行的大事务,改成
WHILE循环 +TOP (50)分片 + 显式COMMIT;把 RPC 调用、日志写入、JSON 序列化等非 DB 操作全部移到COMMIT之后
最易被忽略的一点:死锁日志里的 inputbuf 可能只截断前几百字符,看不出动态拼接的真实条件;务必结合 sys.dm_exec_sql_text 拿完整语句,否则优化永远在猜。

















