必须提前部署扩展事件捕获xml_deadlock_report,常驻运行并存至event_file;解析XML重点查process.inputbuf和resource-list.objectname;结合DMV追溯完整调用链;修复需优化索引、统一多表访问顺序、拆分大事务。

直接启用扩展事件捕获死锁图,而不是等报错后再查
SQL Server 不会在死锁发生时自动记录完整上下文,ERRORLOG 里只留一行“事务被选为死锁牺牲者”,根本看不出谁在卡谁。必须提前部署扩展事件(Extended Events),让它在 xml_deadlock_report 事件触发时实时抓取 XML 死锁图——这是唯一能还原循环等待链的原始数据源。
关键点:不能依赖事后补救,必须常驻运行。死锁是瞬时事件,等你登录上去再开监控,早就错过了。
- 用
CREATE EVENT SESSION创建专用会话,目标设为event_file,避免写入环形缓冲区(ring_buffer)导致日志被覆盖 - 路径要指定可写的本地目录,比如
N'C:\XEvents\deadlock.xel',注意 SQL Server 服务账户需有写权限 - 务必开启
STARTUP_STATE = ON,否则实例重启后事件会话自动停用 - 不建议同时启用
sqlserver.lock_deadlock(已弃用)或deadlock_graph(旧版 Profiler 事件),它们不提供结构化 XML
从 .xel 文件里提取并解析死锁图,重点看 process 和 resource-list
死锁图不是图形界面里的流程图,而是嵌套 XML。用 SSMS 打开 .xel 文件后右键“查看死锁图”,它只是可视化渲染;真正要定位根因,得手动展开 XML 查两个核心节点:
-
process节点里的inputbuf:显示每个会话最后执行的语句,比如EXEC dbo.UpdateOrderStatus @order_id = 123,这就是存储过程入口点 -
resource-list里的objectname和indexname:指出锁冲突发生在哪张表、哪个索引上,比如dbo.Orders.PK_Orders;如果是KEY类型,说明是行级锁冲突;若是PAGE或OBJECT,大概率是索引缺失导致锁升级 - 比对两个
process的waitresource和各自持有的owner-id,就能确认 A 在等 B 锁的某行,B 又在等 A 锁的另一行
结合 DMV 追踪到具体存储过程调用栈,别只信 inputbuf 里的 EXEC
inputbuf 显示的是最外层语句,但真实死锁往往藏在嵌套调用里。比如 EXEC dbo.ProcessBatch 里面又调了 dbo.ValidateInventory 和 dbo.ReserveStock,而死锁实际发生在第二个子过程里。这时候光看 inputbuf 会误判根因。
正确做法是用以下 DMV 关联查出完整调用链:
- 从
sys.dm_exec_requests拿到死锁发生时的session_id和sql_handle - 用
sys.dm_exec_sql_text(sql_handle)查当前正在执行的语句(可能已是中间状态) - 再用
sys.dm_exec_query_plan(plan_handle)提取执行计划 XML,搜索<RelOp.*ProcedureName=".*?">,能定位到具体是哪个存储过程的哪一步在持锁 - 特别注意触发器和外键级联操作——它们不会出现在
inputbuf,但会悄悄引入额外表访问,破坏统一访问顺序
死锁排查后改存储过程,三处必须动:索引、访问顺序、事务粒度
拿到死锁图只是开始,改代码才是关键。很多同学修复时只加 WITH (NOLOCK) 或 SET LOCK_TIMEOUT,结果下一次死锁照旧。真正有效的改动集中在三个硬点上:
-
索引优化:让
WHERE条件走索引查找,避免全表扫描引发大量行锁甚至锁升级。例如UPDATE Orders SET status = 'shipped' WHERE order_id = @id必须确保order_id有单列索引或作为联合索引首列 -
统一访问顺序:所有涉及多表更新的存储过程,必须严格按同一物理顺序访问表,比如总是先
Customers→ 再Orders→ 最后OrderItems;哪怕中间有JOIN或子查询,也要用FORCE ORDER或重写逻辑对齐 -
拆分大事务:一个存储过程里更新 5000 行订单,不要包在单个
BEGIN TRAN里。改用WHILE+TOP (100)+ 显式COMMIT分片,把锁持有时间压到毫秒级
死锁图里看到的从来不是最终原因,而是锁行为失控的结果。真正容易被忽略的是:存储过程里隐式的事务边界(比如没写 SET XACT_ABORT ON)、未捕获的异常导致 ROLLBACK 被跳过、以及跨库调用时分布式事务的锁范围扩大。这些细节不检查,改完索引照样死锁。

















