死锁图中需先看process的inputbuf定位SQL语句,再对照resource-list的KEY/PAGE/OBJECT资源类型;按统一顺序访问表可破循环等待,UPDLOCK仅用于读后续必更新场景,日志写入、临时表创建等辅助操作易引发死锁。

怎么看死锁图里的 process 和 resource-list
SQL Server 报错 Deadlock encountered ... Deadlock graph 时,XML 格式的死锁图里最关键是两个部分:process(谁卡住了)和 resource-list(卡在什么资源上)。每个 process 节点带 spid、inputbuf(最后执行的语句)、executionStack(正在跑的语句栈),而 resource-list 会明确标出是 KEY(某行)、PAGE(某数据页)还是 OBJECT(整张表)。别只看“谁被选为 victim”,先比对两个 process 的 inputbuf —— 如果一个是 UPDATE Orders SET status = 'shipped' WHERE order_id = 123,另一个是 SELECT * FROM Orders WITH (UPDLOCK) WHERE customer_id = 456,基本就能定位是读写交叉导致的锁等待链。
为什么按相同顺序访问表能减少死锁
死锁本质是循环等待:A 持有 X 锁等待 Y,B 持有 Y 锁等待 X。只要所有存储过程访问多张表时严格遵循同一顺序(比如总是先 Customers → 再 Orders → 最后 OrderItems),就不可能形成环。实际中容易忽略的是隐式访问顺序:
-
JOIN顺序不等于执行顺序,SQL Server 可能重排;真正起作用的是WHERE条件中涉及的索引查找路径 - 触发器、外键级联操作会悄悄引入额外表访问,必须把它们也纳入统一顺序约束
- 如果用
IN (SELECT ...)子查询,子查询里涉及的表也要和主逻辑顺序一致,否则等于开了个“顺序后门”
UPDLOCK 和 HOLDLOCK 到底该加在哪条语句上
不是所有读操作都需要加锁提示,乱加反而扩大锁范围。关键原则:只在“读之后必然要更新”的场景下,在最初的 SELECT 上加 UPDLOCK(避免后续 UPDATE 等待 S 锁升级),且必须配合事务包裹。常见错误:
- 在
IF EXISTS (SELECT ...)后直接INSERT,但EXISTS用的是 S 锁,插入前可能已被其他事务插入 —— 正确做法是SELECT ... WITH (UPDLOCK, HOLDLOCK) WHERE ...查一行,再判断是否插入 - 误以为
HOLDLOCK能锁住整个表,其实它只是把 S/U 锁持续到事务结束,对未命中的查询范围无效(需要UPDLOCK + HOLDLOCK + SERIALIZABLE才能防幻读) - 在存储过程中混用
NOLOCK和UPDLOCK—— 前者绕过锁,后者申请锁,逻辑自相矛盾
存储过程里哪些地方最容易埋下死锁隐患
死锁高发点往往不在显眼的 UPDATE,而在看似无害的辅助操作:
- 日志表写入:如果每个业务存储过程都往
AuditLog插一条记录,且没建好聚集索引(比如用datetime当主键),高并发下INSERT会争抢最后一页,形成“热点页死锁” - 临时表创建:多个存储过程同时执行
SELECT ... INTO #temp,SQL Server 需要分配 IAM 页面,可能触发系统表锁竞争 - 游标遍历:默认是
DECLARE cursor_name CURSOR FOR SELECT ...,底层用 S 锁逐行取,若另一进程正批量更新同一表,极易锁住游标正在读的行 - 嵌套调用:存储过程 A 调用 B,B 又调用 C,如果各自事务边界不清晰(比如 B 用了
SAVE TRANSACTION但没正确回滚),锁持有时间不可控
复杂点在于:死锁不是性能问题,而是逻辑竞态。即使单次执行没问题,压测时几十个并发一跑,顺序微小差异就会暴露。盯住 inputbuf 和访问路径顺序,比调优执行计划更管用。










