死锁本质是并发事务循环等待资源,SQL Server主动检测并终止(报错1205);需从锁粒度、顺序、持有时间三方面优化:建精准索引、统一多表访问顺序、拆分大事务。

死锁不是存储过程写得有问题,而是多个并发执行的事务在争夺同一组资源时,恰好形成了循环等待——SQL Server 会在 1–5 毫秒内主动检测并终止其中一个(报错 1205),你重试、加 SET LOCK_TIMEOUT 或改隔离级别都绕不开这个本质。真正要动手,得从锁的粒度、顺序和持有时间三处切。
怎么看清谁卡了谁:从死锁图里抓 inputbuf 和 resource-list
收到 Transaction was deadlocked... rerun the transaction 后,别急着改代码。先确认是否真死锁(不是普通阻塞),再定位具体语句和资源:
- 启用死锁捕获:
DBCC TRACEON(1222, -1)(需 sysadmin 权限),错误日志会自动生成 XML 死锁图 - 重点看每个
节点下的inputbuf:它显示该会话最后执行的那条语句,比如UPDATE Orders SET status = 'shipped' WHERE order_id = 123 - 再看
里的resource类型:KEY是行锁(安全),PAGE是页锁(风险中),OBJECT或TAB是整表锁(高危) - 比对两个 process 的
inputbuf和它们各自等待的resource,就能还原循环链:A 在等 B 锁的某 KEY,B 又在等 A 锁的另一 KEY
为什么 SET LOCK_TIMEOUT 对死锁完全无效
很多人在存储过程开头加 SET LOCK_TIMEOUT 5000,以为能“防死锁”,结果照样报 1205。原因很直接:
-
LOCK_TIMEOUT控制的是单条语句被别人锁住后最多等多久(超时抛1204),而死锁是引擎在毫秒级内主动检测并干掉事务,根本没走到“等超时”这步 - 更危险的是把它和重试混用:第一次死锁回滚后,数据状态可能已被其他事务修改,重试时 WHERE 条件不匹配或更新了错误行,业务逻辑就乱了
- 它只适合明确知道会被长期独占的场景,比如带
WITH (TABLOCK)的报表查询;对解决死锁毫无意义
真正有效的三个落地动作:索引、顺序、拆分
死锁归根结底是锁范围太大 + 锁持有太久 + 访问顺序不一致。必须动具体 SQL 和事务结构:
-
索引优化:让
UPDATE/DELETE精准走索引找行,避免全表扫描锁升级。给WHERE字段建联合索引,顺序按 AND 出现顺序排;删掉无用INCLUDE列(尤其NVARCHAR(MAX)这类大字段),否则容易触发锁升级为页锁 -
统一访问顺序:所有涉及多表的存储过程,必须严格按同一物理顺序访问表(如总是
Customers → Orders → OrderItems)。注意外键级联、触发器、IN (SELECT ...)子查询也会悄悄引入额外表,这些也得纳入顺序约束 -
拆分大事务:一次更新 1000 行不要包在一个事务里。改用
WHILE循环 +TOP (50)分片 + 显式COMMIT;把 RPC 调用、JSON 序列化、日志写入等非 DB 操作全部移到COMMIT之后
最容易被忽略的是“访问顺序”这个点:它不体现在 SQL 语法里,而藏在调用链中——一个存储过程调另一个,另一个又触发了外键级联更新,整个链条上所有表的访问次序必须全局一致,否则压测时才突然冒出来。别只盯着自己写的那几行 UPDATE。











