资讯动态

SQL Server死锁排查实战:读懂死锁图与扩展事件破解1205

发布时间:2026/10/13 23:46:00 来源:尧图企业网站定制
简介这是一份面向SQL Server数据库开发与运维人员的死锁排查技术资料以一类由非聚集索引中包含列及大型可变长字符字段引发的“奇怪死锁”为案例完整还原了问题重现、原因假设、实验对比和解决过程。资料详细对比了三种测试场景说明更新语句在行锁、非聚集索引与大型字段组合下如何产生锁等待与相互阻塞并给出移除包含列选项或修改字段类型后的差异结论帮助读者理解索引设计对死锁的影响。文中先后介绍了开启1222跟踪标记、用SQL Profiler按会话编号筛选锁事件与SQL事件、以及读取错误日志中的死锁记录等典型分析方法还结合死锁图与语句堆栈进行解读适合想深入掌握SQL Server锁机制与死锁调优的中高级读者。整份资源打包为一个docx文档压缩后大小约六百余KB内容紧凑、步骤完整包含建表脚本、更新循环和三类测试的具体结果。该资源目前已有326人学习阅读后可建立一套可复用的死锁分析思路用于排查生产环境中的同类问题。1. 一个查了整晚的 Deadlock先认清它不是玄学SQL Server 上的 deadlock 报错1205有个招人烦的特点日志里确实留着死锁图图里也确实有两个会话可你把每条 SQL 单独拿出来跑一遍都正常锁的获取顺序看着也不该撞车。我第一次被它困住是在维护模拟项目X的时候凌晨收到告警打开错误日志看到一张残缺的死锁图——两个进程各持一把 KEY 锁资源列表里却只有一个锁条目环形等待怎么都画不完整。折腾到天亮才明白不是死锁图骗人是我没读懂它省略掉的那半截信息。这篇笔记讲的就是这类「奇怪死锁」的完整分析方法写给被线上 1205 折磨的 DBA 和后端开发也写给刚接触锁机制、想读懂死锁图的初学者。我会从死锁的判定原理讲到扩展事件抓取、死锁图 XML 解析再落到我踩过的几个坑。读完你能独立完成一次死锁归因而不是对着图猜。2. 死锁原理与死锁图SQL Server 在等谁、卡在哪2.1 死锁的本质等待成环而不是锁不够先纠正一个最常见的误解死锁不是「锁不够用」而是「两个或多个会话在等待一条环形的资源链且 SQL Server 判定这条链永远解不开」。教科书例子是会话 A 持有订单表的行锁要申请客户表的行锁会话 B 持有客户表的行锁要申请订单表的行锁。A 等 B、B 等 A谁也不让步这就是标准的环形等待。SQL Server 的检测器每隔几秒扫描一次锁等待图发现成环之后选一个牺牲者回滚被选中方收到 1205。但生产环境里的死锁远比这个示例难读原因有三个。第一锁粒度不统一——A 等的是行锁KEYB 等的可能是页锁PAGE或表锁OBJECT你得先把粒度换算到同一层才能看懂冲突。第二进程在等锁的同时还在做别的等待比如等待日志写入、等待内存、等待下级线程死锁图只记录与本次死锁裁决有关的等待其他等待不会出现在环里。第三也是最常见的一个 UPDATE 语句会按执行计划里的顺序获取多把锁计划不同拿锁顺序就不同两个语句哪怕更新的是同一批行也可能因为走的索引不一样而持有完全相反的锁顺序。所以在分析任何死锁之前我习惯先确认四件事当前隔离级别是什么因为隔离级别决定锁的持有时长语句实际走的哪个索引因为索引决定拿锁顺序有没有隐式转换因为类型不匹配会导致索引失效、锁定范围变大有没有锁升级发生因为行锁一旦升级成表锁冲突面就完全不同。做完这四件事一半的死锁原因已经浮出水面。锁的模式只需要盯住几个常用的S共享读锁、X排他写锁、U更新锁UPDATE 在定位要修改的行时先用 U避免两个事务同时把 S 升级成 X 造成阻塞、以及 IX/SIX 这一类意向锁。死锁图里 owner-list 和 waiter-list 的 mode 字段写的就是这些缩写。看不懂 mode 也没关系先记住一个方向两个进程都持有对方下一步要用的锁且至少一方是 X 或 U这个死锁十有八九是写写冲突如果全是 S基本不会死锁因为共享锁之间互不排斥。还有一个绕不开的概念牺牲者怎么选。默认情况下回滚成本低的事务更容易被牺牲比如语句级事务而不是长事务你也可以用 SET DEADLOCK_PRIORITY 显式指定优先级。我见过一个生产事故有人把夜里批处理作业的优先级调高结果所有前台事务在冲突时反复当牺牲者现象就是「一到整点前台就集体报 1205」。所以看到死锁先别急着调优先级先想清楚你希望谁活下来再决定是否干预。锁升级是让死锁变奇怪的另一只手。单个语句对同一个对象拿的锁超过一定数量常见的触发线是 5000 个锁左右实际由内存阈值决定SQL Server 会把行锁合并升级成表锁以减少锁管理器开销。升级之后粒度变大本来不该冲突的两个事务突然开始抢同一张表。死锁图里如果出现 OBJECT 类型的锁且同一进程带了大量行锁记录基本可以断定锁升级参与了这场死锁。2.2 死锁图 XML 里有哪些关键字段从 process 到 resourceSQL Server 抓到的死锁图是一个 XML 文档错误日志、扩展事件目标文件里存的都是这份 XML。打开一张完整的死锁图结构分三段victim-list 告诉你谁被牺牲process-list 列出所有参与者以及每个会话当时正在执行的语句resource-list 列出争夺的锁资源每种锁下面又有 owner-list持锁人和 waiter-list等待人。我第一次手动解析时觉得它像天书其实抓三个信息就够了每个 process 的 id、inputbuf 里的 SQL 文本、以及每个 resource 下 owner 和 waiter 的配对。把所有 owner 和 waiter 连起来画图就会看到一个或多个环。下面是一段典型死锁图的核心片段我按字段加了注释deadlock victim-list victimProcess idprocess2b1a4584688 / /victim-list process-list !-- 两个会话各持一把锁同时各自等待对方持有的锁 -- process idprocess2b1a4584688 spid62 executionStack frame procnamedbo.usp_UpdateOrdersByCustomer line12 / /executionStack inputbufUPDATE dbo.Orders SET Status s WHERE CustomerId cid/inputbuf /process process idprocess2b1a4584690 spid71 executionStack frame procnamedbo.usp_UpdateOrdersByOrderId line18 / /executionStack inputbufUPDATE dbo.Orders SET Status s WHERE OrderId oid/inputbuf /process /process-list resource-list !-- 同一个订单行spid 71 持有 X 锁spid 62 正在等待这把锁 -- keylock hobtid72057594050838528 objectnamedbo.Orders indexnamePK_Orders owner-list owner idprocess2b1a4584690 modeX / /owner-list waiter-list waiter idprocess2b1a4584688 modeU / /waiter-list /keylock /resource-list /deadlock这段 XML 里最容易被忽略的是 waiter 的 modeU。U 锁表示「我想更新这一行正在确认当前行的值」确认后升级为 X 并持有到事务结束。图里 U 等 X说明 spid 62 已经定位到了这一行但 spid 71 握着 X它只能等一个事务结束。真正「奇怪」的地方在这上面的 XML 只画出一个资源。按这个图62 在等 71但 71 并没有在等 62——环的另一半不见了。答案通常是这张图被截断或者第二个资源因为锁升级被合并成了 OBJECT 条目。第四章我会用一个完整案例演示怎么把另一半找回来。这里先记住结论看到只有单向等待的死锁图第一反应不是下结论而是去查完整历史记录和两个会话的完整事务上下文。为了把死锁图里的 owner-waiter 配对批量抽出来我常用一段 XQuery把每个资源节点下的持锁人和等待人平铺成行DECLARE DeadlockXml XML Ndeadlock…省略…/deadlock; SELECT r.value(name(.), varchar(20)) AS ResourceType, COALESCE(r.value(objectname,varchar(128)), r.value(fileid,varchar(20))) AS ResourceName, o.value(id, varchar(50)) AS LockOwner, o.value(mode, varchar(10)) AS OwnerMode, w.value(id, varchar(50)) AS LockWaiter, w.value(mode, varchar(10)) AS WaiterMode FROM DeadlockXml.nodes(//resource-list/*) AS R(r) CROSS APPLY r.nodes(owner-list/owner) AS O(o) CROSS APPLY r.nodes(waiter-list/waiter) AS W(w);这段脚本做的事情很简单nodes 方法把 resource-list 下的每个资源拆成一行owner-list 和 waiter-list 各拆成多行最后按资源做交叉关联。输出的每一行就是一条「某资源上谁持锁、谁在等」的记录。把同一次死锁的多行记录拼起来环形等待在哪一环断裂一眼就能看到。我用它处理过一次 8 个进程参与的大型死锁手工画图要半小时脚本跑完 10 秒出结果。3. 用扩展事件抓现场最小命令与抓取策略3.1 开启 system_health 与自定义采集会话先低成本覆盖再按需加码抓死锁有个前提你必须在死锁发生之前就把「摄像头」装好否则一切都是事后推理。好消息是 SQL Server 自带的 system_health 会话一直在后台运行它默认采集 xml_deadlock_report 事件。很多新手不知道这一点白等了几天错误日志。先查系统里有哪些扩展事件会话、是否在运行SELECT s.name, s.create_time, s.is_running FROM sys.dm_xe_sessions s WHERE s.name IN (system_health, AlwaysOn_health); -- 如果 system_health 没在运行说明服务配置被改过手工启动 -- ALTER EVENT SESSION system_health ON SERVER STATE START;system_health 的采集目标是 ring_buffer也就是内存里的一块环形缓存。优点是零维护缺点是容量有限死锁少时够用死锁一多或时间一长旧事件会被覆盖。所以我的习惯是system_health 用于事后确认「这里发生过死锁」要拿连续完整的死锁图做分析必须建一个自定义会话把事件写到文件里。下面是我在模拟项目X上用的最小采集配置。关键点有三个事件选 sqlserver.xml_deadlock_report目标用 package0.event_fileSTARTUP_STATE 设为 ON保证实例重启后自动拉起来。CREATE EVENT SESSION [deadlock_capture] ON SERVER ADD EVENT sqlserver.xml_deadlock_report ( ACTION (sqlserver.session_id, sqlserver.tsql_stack, sqlserver.client_app_name) ) ADD TARGET package0.event_file ( SET filename ND:\XEvent\deadlock_capture, max_file_size 50, -- 单个文件 50MB max_rollover_files 10 -- 最多保留 10 个文件翻转覆盖 ) WITH (MAX_MEMORY 8 MB, MAX_DISPATCH_LATENCY 5 SECONDS, STARTUP_STATE ON); ALTER EVENT SESSION [deadlock_capture] ON SERVER STATE START;这里每个参数都不是随手写的踩过的坑值得交代清楚参数作用我的设置与理由filename目标文件路径前缀实际文件是 deadlock_capture_0_xxx.xel目录必须存在且服务账户有写权限max_file_size单个文件上限50MB一张死锁图 XML 几十 KB够存几千张max_rollover_files翻转文件数10防止文件无限膨胀拖垮磁盘MAX_MEMORY事件缓冲内存上限8MB 起步太低会丢事件MAX_DISPATCH_LATENCY缓冲滞留时间5 秒太大会出现「死锁发生了但文件里还没写到」的假象STARTUP_STATE是否随实例启动ON这是「后悔药」——会话因重启断了下次死锁就没有现场读取 event_file 目标里的内容用系统函数 sys.fn_xe_file_target_read_file 按文件顺序读返回的 event_data 是 XML 文本需要 CAST 成 XML 再解析SELECT CAST(event_data AS XML) AS DeadlockXml FROM sys.fn_xe_file_target_read_file( ND:\XEvent\deadlock_capture*.xel, NULL, NULL, NULL ) WHERE object_name xml_deadlock_report;注意两个细节一是文件名通配符必须加函数按前缀匹配所有翻转文件二是结果按写入顺序返回实际用时建议按 XML 里的 timestamp 属性排序不要依赖返回顺序。我一般在 Management Studio 里把 DeadlockXml 列的结果另存为 .xml再用格式化工具展开看原始的换行会让人抓狂。3.2 从 ring_buffer 导出死锁图一条 XQuery 搞定如果死锁只是偶发一次你不想为了它专门建会话也可以直接从 system_health 的 ring_buffer 里捞历史死锁。做法是把 target_data 转成 XML再用 XQuery 过滤出 xml_deadlock_report 事件。这段脚本我几乎原样背下来了WITH RingData AS ( SELECT CAST(t.target_data AS XML) AS Data FROM sys.dm_xe_sessions s JOIN sys.dm_xe_session_targets t ON s.address t.event_session_address WHERE s.name system_health AND t.target_name ring_buffer ) SELECT n.value(timestamp, datetime2) AS EventTime, n.value((data/value)[1], varchar(max)) AS DeadlockXml FROM RingData CROSS APPLY Data.nodes(/RingBufferTarget/event) AS p(n) WHERE n.value(name, varchar(50)) xml_deadlock_report ORDER BY EventTime DESC;核心在 CROSS APPLY Data.nodes(...) 这一句nodes 方法把 ring_buffer 根节点下的每个 event 元素拆成一行WHERE 过滤事件名data/value 里就是完整的死锁图 XML。这段查询的输出经常是超大文本网格显示不全我习惯把结果导出成 .xml 文件再格式化。需要清醒的一点ring_buffer 受会话 MAX_MEMORY 限制默认只保留最近一批事件。如果你的系统死锁频繁这里只能查到最近几次。这再次说明高频死锁场景必须依赖 event_file 目标。我还习惯同时把 trace flag 1222 开着它的作用是把死锁图以完整文本形式写进错误日志包含每个进程的完整锁列表和资源描述在 XML 被截断或损坏时是最可靠的后备DBCC TRACEON(1222, -1); -- 全局开启-1 表示对所有会话生效 DBCC TRACEOFF(1222, -1);特别注意1222 是仍受支持的传统 trace flagDBCC TRACEON 对它是有效的。错误日志里的 1222 输出是纯文本没有 XML 那种嵌套结构读起来反而直观。我现在每次分析死锁都会同时开三份材料扩展事件文件、1222 文本输出、两个会话的完整调用栈缺一不可。这个习惯帮我少走了很多弯路。4. 解开那个「奇怪」死锁环形等待的另一半藏在哪4.1 模拟项目X的现场还原两个 UPDATE 如何互相踩到脚现在进入正题。当时模拟项目X的报错很规律错误日志里每条 1205 都发生在存储过程 usp_UpdateOrdersByCustomer下面简称 ProcC和 usp_UpdateOrdersByOrderId简称 ProcO之间。两个过程都只更新一张订单表 Orders简化后的表结构CREATE TABLE dbo.Orders ( OrderId INT PRIMARY KEY, CustomerId INT NOT NULL, Status TINYINT NOT NULL, UpdateTime DATETIME NOT NULL ); -- 注意Status 被包含进非聚集索引更新 Status 会同时写这个索引 CREATE NONCLUSTERED INDEX IX_Orders_CustomerId ON dbo.Orders(CustomerId) INCLUDE (Status);ProcC 按 CustomerId 更新状态ProcO 按 OrderId 更新状态这是业务上再正常不过的两个入口。单看每一条 SQL谁都会觉得「它们只是改了同一张表的不同入口锁冲突不至于成环」。可实际死锁图显示 ProcC 在等一把由 ProcO 持有的 KEY 锁而 ProcO 又等在另一把锁上。我最初拿到的 XML 图就是第二章展示的那张单向图根本拼不出环。把 1222 的文本输出调出来之后第二个资源出现了ProcC 的执行计划先撞进非聚集索引 IX_Orders_CustomerId它已经在这把索引的 KEY 上持有了锁ProcO 从主键 PK_Orders 进来已经在聚集索引的行上持有了锁。两边各自抓住一半资源然后同时伸手要另一半获取顺序ProcC按 CustomerId 更新ProcO按 OrderId 更新第 1 把锁IX_Orders_CustomerId 的 KEYXPK_Orders 的 KEYX第 2 把锁PK_Orders 的 KEYXIX_Orders_CustomerId 的 KEYX两个资源一拼环才完整ProcC 持有非聚集索引 KEY等待聚集索引行锁ProcO 持有聚集索引行锁等待非聚集索引 KEY。死锁不是数据冲突是两条更新路径的拿锁顺序互相颠倒。如果只盯着第一张 XML 图你永远看不到第二个资源因为 xml_deadlock_report 事件在某些版本的快照里只记录最终导致裁决的那部分资源对前面的争夺被省略了。这也是我反复强调用 1222 文本输出和扩展事件双份印证的原因——一张图不能当全部证据。4.2 定位根因执行计划决定锁顺序锁顺序决定生死死锁图确认了锁资源但根因得往执行计划里挖。我把两个存储过程实际执行时的计划拉出来对比关键差异在 ProcC优化器选择了非聚集索引 IX_Orders_CustomerId 做 Seek定位到所有目标订单然后对聚集索引做 Lookup。Lookup 意味着它先拿非聚集索引 KEY 的锁再回头拿聚集索引行的锁。而 ProcO 按 OrderId 走主键 Seek先拿聚集索引 KEY之后再在更新过程中修改非聚集索引的叶子行。也就是说ProcC 是「非聚集 → 聚集」ProcO 是「聚集 → 非聚集」正好相反。当两个事务更新同一批订单时各自抓住半把锁谁也不肯先放死锁就从「看起来不该发生」变成了必然。注意一个前提这出戏能上演是因为 Status 被 INCLUDE 进了非聚集索引——更新 Status 这个普通列逻辑上等同于要在两个索引里改数据两个索引都会被加锁。如果 Status 不在任何非聚集索引里ProcC 只需锁聚集索引行根本不会和非聚集索引的 KEY 纠缠死锁也就不会发生。验证这个判断我用了三个手段。第一用 SET STATISTICS IO, TIME ON 单独跑两个过程的小样本确认索引使用情况和 Lookup 次数。第二把两个过程放在两个会话里手动构造同一批数据逐步执行在每个关键语句前后查询锁视图观察锁从无到有、从 U 到 X 的变化过程SELECT request_session_id, resource_type, resource_description, request_mode, request_status FROM sys.dm_tran_locks WHERE resource_database_id DB_ID(); -- 只看当前库避免混入系统库的锁注意这个视图只是瞬间快照两个会话交错执行时必须在每个关键语句前后分别查询才能看到拿锁顺序。我习惯把结果按 request_session_id 分组、按 resource_description 排序两列对齐之后先拿谁、在等谁一目了然。第三也是最快的验证给 ProcC 加锁提示强制它改走主键后死锁立刻消失——虽然这不能当正式方案但足以证明根因在访问路径不在业务数据。4.3 可行的修复从锁提示到程序改版根因定位之后修复方案按侵入性从小到大排有三个我最后选了第二个。第一个是加锁提示让 ProcC 强制用聚集索引定位UPDATE dbo.Orders WITH (INDEX PK_Orders) SET Status s, UpdateTime GETDATE() WHERE CustomerId cid;锁提示不是银弹ProcC 的过滤条件是 CustomerId主键是 OrderId强制用主键意味着扫描整张表再过滤数据量大时性能不可接受。它只适合用来验证根因不适合当长期方案。第二个方案是改 ProcC 的逻辑先查出该客户的所有 OrderId再按主键逐批更新。这样两个过程最终都从主键入口拿锁锁顺序统一为「聚集 → 非聚集」BEGIN TRANSACTION; DECLARE ids TABLE (OrderId INT PRIMARY KEY); INSERT INTO ids (OrderId) SELECT OrderId FROM dbo.Orders WHERE CustomerId cid; UPDATE o SET Status s, UpdateTime GETDATE() FROM dbo.Orders o JOIN ids i ON o.OrderId i.OrderId; COMMIT;这段改动的要点在 UPDATE 的 JOIN 写法目标表 Orders 通过主键与表变量关联优化器会走聚集索引定位行锁顺序与 ProcO 一致。执行完之后需要复查执行计划确认 Orders 确实是按主键 Seek而不是对 Orders 全表扫描——计划变了锁顺序就可能变回去。第三个方案是业务层加重试。死锁是事务型数据库的正常现象再严密的规约也挡不住极端并发给 1205 加自动重试是最后的兜底DECLARE retry INT 0; WHILE retry 3 BEGIN BEGIN TRY BEGIN TRANSACTION; -- 这里放更新逻辑 COMMIT; BREAK; END TRY BEGIN CATCH IF ERROR_NUMBER() 1205 AND retry 3 BEGIN SET retry retry 1; ROLLBACK; WAITFOR DELAY 00:00:00.100; END ELSE THROW; END CATCH END我最终采用的是方案二。上线后死锁从一天几十次降到零三个月没有复发。这个案例可以推广成一条判断准则当死锁图里出现同一个表的聚集索引和非聚集索引上的 KEY 锁且两个更新语句的过滤条件分别命中不同索引时优先怀疑锁顺序颠倒而不是先怀疑事务太长或隔离级别。5. Deadlock 排查避坑指南5 个让我翻过车的细节5.1 现象图里只有一个资源环拼不完整这是我入坑最深的一条。现象是 1205 错误日志里的死锁图只有一条 resource-list 条目像是「A 在等 B但 B 没在等任何人」怎么画都画不出环。原因正如第四章说的xml_deadlock_report 的 XML 在某些情况下只保留最终裁决所需的资源快照省略了环里其他锁或者把多把锁合并进了同一个 keylock 条目另外如果两个进程之间隔着第三把锁而这把锁由另一个没参与本次死锁裁决的进程持有图里的连线就可能被简化。解决方法是开 1222 trace flag拿到文本版完整锁列表再和扩展事件文件互相印证如果还是对不上就把两个会话的完整事务上下文包括它们嵌套调用的其他存储过程拉出来人工重建等待链。我后来处理过一起 8 个进程参与的死锁单看 XML 图永远拼不出来靠的就是 1222 的全量输出。5.2 现象日志里找不到这次死锁的任何记录死锁确实发生了应用明确报了 1205但去看 system_health 的 ring_buffer里面的 xml_deadlock_report 要么没有要么被新事件覆盖了。原因有两个ring_buffer 容量有限死锁一多最早的会被挤掉或者 system_health 会话被手工关闭过而自定义的 event_file 会话没设 STARTUP_STATE ON实例一重启采集就断了。解决方法是把自定义会话的 STARTUP_STATE 打开并每周检查一次会话运行状态。我的习惯是顺手加一条查询把扩展事件会话的运行状态汇总到邮件报表里断了能马上知道。另外还要注意把 event_file 的目标放在独立磁盘分区避免文件系统写满导致事件静默丢失。这条的教训是监控工具的可用性比监控工具的功能更重要。5.3 现象两个 UPDATE 明明更新不同行怎么也死锁这是最让新手崩溃的场景A 更新 OrderId1001B 更新 OrderId1002提交前互不相关可还是 1205。原因通常是两个语句都更新了同一个非聚集索引里的条目比如我们把 Status 放在了 INCLUDE 列里B 更新 1002 行时改写了非聚集索引的叶子页而这个叶子页恰好和 A 要更新的 1001 行在同一个页面区间页级或者相邻 KEY 的锁把两个事务拴在了一起。解决方法是先看死锁图里锁挂在哪个 indexname 上——如果挂的是某个非聚集索引而不是主键就要检查更新语句是否触碰了该索引的键列或包含列再看锁粒度是 KEY 还是 PAGE如果出现 PageLock 或 ExtentLock考虑是不是索引碎片导致逻辑上的相邻行散落到了相邻页面。我自己遇到的一次就是 UPDATE 修改了 CustomerId 字段触发了非聚集索引的页拆分把不少不相干的行卷了进来。5.4 现象开了 READ_COMMITTED_SNAPSHOT死锁反而变多有人以为行版本控制能解决一切死锁把 READ_COMMITTED_SNAPSHOT 打开结果死锁非但没消失读写冲突变成了更集中的写写冲突。原因在于快照隔离只消除了「读被写阻塞」这条等待链两个 UPDATE 对同一行的写写竞争依然存在依然要拿 X 锁而在 SNAPSHOT 隔离级别下更新事务还会因为版本冲突直接报 3966表现更吓人。解决方法是把期望放对位置RCSI 解决的是「读到不一致数据」解决不了「两个写互相打架」。写写冲突要靠锁顺序规约、索引优化和重试机制不能指望隔离级别救场。如果业务允许把高频更新事务缩短或者改成「先读版本号根据版本号决定是否更新」的乐观并发流程才是正解。5.5 现象死锁图指向的存储过程和你实际怀疑的不是同一个很多 1205 的 inputbuf 显示的是最外层调用者比如一个作业调用了 usp_StartSync而真正发生锁竞争的是它内部嵌套调用的 usp_WriteAudit死锁图的 frame procname 只标了最后一个开始执行的存储过程容易被误判。解决方法是看 executionStack 里的完整调用栈把 procname 和 line 逐层列出来再结合 sql_handle 反查语句文本如果栈里只有一条多半是被截断或调用方是 ad hoc 语句需要把 client_app_name 和 session_id 连起来在应用日志里找调用源头。这个坑让我养成了一个习惯看到死锁图先看调用栈再下结论而不是盯着 inputbuf 里的第一行 SQL 猜。6. 让死锁不再「奇怪」锁顺序规约与主动监控脚本6.1 把锁顺序变成代码评审的检查项分析完奇怪的死锁之后最有价值的产出不是修复一个案例而是把教训沉淀成约束。我在模拟项目X里的做法是把所有写 Orders 表的存储过程列成一张清单标明每个过程最终以什么顺序接触聚集索引和非聚集索引然后约定「凡是更新同一张表的多个过程必须按相同顺序获取索引上的锁」。这条规约在代码评审阶段特别好用——只要看到 WHERE 条件走的是不同的索引就多问一句「这个过程的锁顺序和另一个入口一致吗」。对无法调整顺序的核心事务再增加一个统一入口存储过程做行锁排序业务侧只调它。除了顺序规约值得养成习惯的还有事务边界审核要求每个事务里只做「定位数据」和「更新数据」两件事不要夹带长耗时的外部调用不要在事务里等用户输入。锁持有时间越短死锁窗口越小。这比任何调优都直观。6.2 一个可留作值班的监控脚本自动报告死锁摘要最后给你一个可以直接值班用的脚本思路用 SQL 代理作业每 10 分钟跑一次读取最近 10 分钟内的死锁图按「涉及的表 索引 语句模式」聚合输出到一张审计表。这样死锁一多你能直接在报表里看到规律而不是翻几百个 .xel 文件。核心逻辑是把扩展事件文件按时间过滤再从死锁图 XML 里提取 objectname 和 indexnameDECLARE Since DATETIME2 DATEADD(MINUTE, -10, SYSDATETIME()); SELECT x.value((//resource-list/keylock/objectname)[1], varchar(128)) AS ObjectName, x.value((//resource-list/keylock/indexname)[1], varchar(128)) AS IndexName, COUNT(*) AS DeadlockCount FROM sys.fn_xe_file_target_read_file( ND:\XEvent\deadlock_capture*.xel, NULL, NULL, NULL ) AS f CROSS APPLY (SELECT CAST(f.event_data AS XML)) AS x(n) WHERE x.value(timestamp, datetime2) Since GROUP BY x.value((//resource-list/keylock/objectname)[1], varchar(128)), x.value((//resource-list/keylock/indexname)[1], varchar(128)) ORDER BY DeadlockCount DESC;这段聚合脚本有两个前提objectname 和 indexname 在每张死锁图里不一定都存在遇到 page lock 或 object lock 时 XML 节点名不同完整统计要把每个 resource 子节点都拆开而且它只统计了 resource-list 里的第一个资源想看完整分布还要按资源节点逐个展开。脚本的意义不在精确而在于把人工翻日志变成趋势告警——当某对索引连续出现在死锁图里就该主动找 DBA 而不是等用户报障了。回看我处理过的这些死锁真正「奇怪」的几乎没有大多是我一开始没看全证据。那次通宵最后变成一条工作习惯任何死锁分析先收集三份材料——扩展事件文件、1222 文本输出、两个会话的完整调用栈然后再画环。环画出来了修复方案基本都是锁顺序问题或访问路径问题根本不用玄学。希望这篇笔记帮到你下次再看到残缺的死锁图至少知道去哪里把另一半找回来。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑