简介:本资源是一份深入剖析SQL Server死锁机制的技术文档,面向数据库开发人员、DBA及中高级SQL性能调优实践者,聚焦一个典型却易被忽视的非聚集索引引发的死锁场景。文档完整复现了因non-clustered index中INCLUDE大字段(varchar(max))与行锁(rowlock)交互导致的循环等待死锁,并系统对比了三种关键干预措施(移除INCLUDE、缩小字段长度、调整索引结构)对死锁消解的影响,同时详述SQL Trace与DBCC TRACEON 1222双路径分析法,含SPID过滤、死锁图解读及sp_readerrorlog日志定位实操。资源为1个694KB的Word文档(.docx),内容结构清晰,含建表脚本、复现步骤、执行计划线索、锁资源堆栈解析及根本原因推演,是理解SQL Server锁升级、索引设计与事务并发冲突的高价值案例材料。目前已有327人学习下载。
1. 这不是锁争用,是执行计划在“演双簧”:一个由varchar(max)+INCLUDE触发的 SQL Server 死锁黑匣子
你有没有遇到过这种场景:两条一模一样的UPDATE语句,并发执行时稳稳当当死锁,但只要把索引里一个INCLUDE(d)去掉,或者把d varchar(max)改成varchar(200),死锁就彻底消失?连ROWLOCK提示都救不了——这不是锁粒度没设对,也不是事务写得太长,而是 SQL Server 在背后悄悄拆分了你的 UPDATE 操作,让两个连接在「找数据」和「改索引」两个阶段错位卡死。这个案例在 SQL Server 2008 到 2019 中均可稳定复现,本质是执行计划分裂导致锁申请顺序不可控,属于典型的“玄学死锁”:表面看是并发冲突,根子上是优化器对大字段 + 覆盖索引的特殊处理逻辑。它专挑那些喜欢用varchar(max)做业务字段、又习惯给非聚集索引加INCLUDE的系统下手——这类设计在 OLTP 场景下极易翻车。本文不讲抽象理论,直接带你从建表、复现、抓锁、读执行计划到定位根因,每一步都可粘贴即跑,所有命令、输出片段、参数含义全部实测验证。如果你正被类似死锁折磨,或想建立一套可落地的 SQL Server 死锁分析肌肉记忆,这篇就是为你写的。
2. 复现环境搭建与三组对照实验:用最小闭环验证死锁触发条件
要真正理解这个死锁,必须亲手把它“养”出来。我们不依赖生产库,也不模拟复杂业务逻辑,只用四条 SQL 就能构建出完全可控的死锁现场。关键在于三组严格对照实验:原始问题组(必死)→ 索引精简组(不死)→ 字段瘦身组(不死)。每组都基于同一张表结构,仅变更一个变量,确保结论干净、可归因。
2.1 创建基础表与双非聚集索引:聚焦INCLUDE(d)的埋点位置
我们创建一张极简表tt,主键为id(聚集索引),并建立两个非聚集索引:ix_a_bc和ix_b_cd。注意INCLUDE(d)是核心变量,它让索引“携带”了d字段的副本,从而避免回表——但代价是更新时必须同步维护该副本。
-- 创建测试表(SQL Server 2008+ 兼容) CREATE TABLE tt ( id INT IDENTITY PRIMARY KEY, a CHAR(36), b CHAR(36), d VARCHAR(MAX) -- 关键:MAX 类型,存储开销大 ); GO -- 创建两个非聚集索引,均 INCLUDE d 字段(问题根源起点) CREATE INDEX ix_a_bc ON tt(a) INCLUDE(d); CREATE INDEX ix_b_cd ON tt(b) INCLUDE(d); GO参数说明:
INCLUDE(d)表示将d字段值物理存储在索引叶级页中,不参与索引排序,但可被SELECT直接取到。对UPDATE而言,这意味着修改d时,不仅主键索引要更新,这两个非聚集索引的叶级页也必须同步写入新值——这是后续锁分裂的物理基础。
2.2 插入测试数据并提取目标值:确保每次复现条件一致
插入 10000 条记录后,我们固定选取id = 10的行作为更新目标。a字段值是NEWID()生成的 GUID,每次运行不同,因此必须先查出来再用于后续UPDATE。这步看似琐碎,却是复现稳定性的命脉。
-- 批量插入 10000 行(使用 GO 10000 实现循环) INSERT INTO tt SELECT NEWID(), 'bbb', 'ddd'; GO 10000 -- 查询第 10 行的 a 值(结果示例:'EF211985-EA72-4A40-81DA-0AAB076E7AA3') SELECT a FROM tt WHERE id = 10;逻辑说明:
GO 10000是 SQLCMD 模式下的批处理指令,表示前一条INSERT语句执行 10000 次。NEWID()确保a值高度离散,使ix_a_bc索引查找走 B-Tree 搜索而非扫描,精准触发单行更新路径。若用NEWID()以外的值(如固定字符串),可能导致索引查找退化为扫描,破坏死锁触发条件。
2.3 三组对照实验脚本:一键切换,秒级验证死锁存在性
我们将三组实验封装为独立脚本块,只需注释/取消注释对应部分,即可在 SSMS 中快速切换。每组均启动两个查询窗口(SPID A 和 SPID B),同时运行无限循环UPDATE。
-- 【实验组1:原始问题】必死锁(varchar(max) + INCLUDE(d)) -- 在窗口A中执行: WHILE 1=1 BEGIN UPDATE tt WITH(ROWLOCK) SET d = 'cd' WHERE a = 'EF211985-EA72-4A40-81DA-0AAB076E7AA3'; WAITFOR DELAY '00:00:00.01'; -- 防止 CPU 占满,便于观察 END -- 在窗口B中执行(相同语句): WHILE 1=1 BEGIN UPDATE tt WITH(ROWLOCK) SET d = 'cd' WHERE a = 'EF211985-EA72-4A40-81DA-0AAB076E7AA3'; WAITFOR DELAY '00:00:00.01'; END-- 【实验组2:索引精简】不死锁(保留 varchar(max),移除 INCLUDE) -- 先执行此清理脚本: DROP INDEX ix_a_bc ON tt; DROP INDEX ix_b_cd ON tt; CREATE INDEX ix_a_bc ON tt(a); -- 移除 INCLUDE(d) CREATE INDEX ix_b_cd ON tt(b); -- 移除 INCLUDE(d) GO -- 然后在窗口A/B中运行同上的 UPDATE 循环 → 无死锁-- 【实验组3:字段瘦身】不死锁(保留 INCLUDE,改 varchar(200)) -- 先执行此清理脚本: DROP INDEX ix_a_bc ON tt; DROP INDEX ix_b_cd ON tt; ALTER TABLE tt ALTER COLUMN d VARCHAR(200); -- 关键变更 CREATE INDEX ix_a_bc ON tt(a) INCLUDE(d); CREATE INDEX ix_b_cd ON tt(b) INCLUDE(d); GO -- 然后在窗口A/B中运行同上的 UPDATE 循环 → 无死锁现象对比总结:
- 实验组1:约 3~10 秒内必然触发死锁,SSMS 弹出错误
Msg 1205, Level 13, State 45, Line X: Deadlock encountered ...;- 实验组2 & 3:持续运行 5 分钟以上,CPU 占用平稳,无任何错误;
- 关键洞察:死锁与
UPDATE语句本身无关,而与d字段的存储方式(MAXvs200)及索引是否“携带”该字段(INCLUDE)强耦合。这直接指向执行计划层面的差异。
3. 死锁捕获与诊断:用DBCC TRACEON(1222,-1)+ SQL Server Profiler 定位锁资源争用链
复现只是第一步,真正的硬功夫在“看见”死锁。SQL Server 提供两套互补工具:TRACEON(1222)输出结构化死锁图到错误日志,SQL Server Profiler 捕获实时锁事件流。二者结合,才能拼出完整因果链。这里不讲概念,只给可抄作业的配置清单和解读口诀。
3.1 开启全局死锁跟踪:让 SQL Server 主动“写日记”
在复现前,必须启用1222跟踪标志,它会将每次死锁的 XML 结构写入 SQL Server 错误日志(ErrorLog)。这是最权威的死锁证据源,比 Profiler 更可靠(Profier 可能漏事件)。
-- 启用全局死锁跟踪(需 sysadmin 权限) DBCC TRACEON(1222, -1); -- 验证是否生效(返回 1222 表示已启用) DBCC TRACESTATUS(1222, -1);参数说明:
-1表示全局生效(所有会话),1222是死锁诊断专用标志。开启后无需重启服务,但建议在测试库操作。关闭用DBCC TRACEOFF(1222, -1)。注意:生产环境慎用,长期开启有轻微性能开销。
3.2 SQL Server Profiler 配置:只抓关键锁事件,拒绝信息过载
Profier 不是用来“录屏”,而是精准过滤出与死锁直接相关的锁生命周期事件。我们只关注Locks类别下的Deadlock Graph、Lock:Acquired、Lock:Released,以及TSQL类别下的SQL:BatchCompleted。重点是SPID 过滤——只监控你手动启动的两个 UPDATE 连接(如 SPID 54 和 60)。
| 配置项 | 推荐设置 | 为什么这样设 |
|---|---|---|
| Events Selection | 勾选Show all events→ 展开Locks→ 勾选Deadlock Graph,Lock:Acquired,Lock:Released;展开TSQL→ 勾选SQL:BatchCompleted | Deadlock Graph提供可视化死锁图;Lock:Acquired/Released记录每个锁的申请/释放时间戳和对象;SQL:BatchCompleted关联 SQL 文本与锁事件 |
| Column Filters | SPID→Like→54,60(填入你实际的两个 SPID) | 避免海量系统锁事件干扰,聚焦问题连接 |
| Event Filtering | ApplicationName→Like→%Management Studio%(可选) | 进一步限定来源,防止其他应用干扰 |
操作提示:启动 Profiler 后,先点击
Run开始监听,再在 SSMS 中打开两个新查询窗口执行UPDATE循环。死锁发生后,Profiler 会立即捕获Deadlock Graph事件,双击即可看到交互式图形(含进程、资源、等待关系)。但图形有时不清晰,此时需结合Lock:Acquired事件列表按StartTime排序,人工梳理锁序列。
3.3 解析1222输出:从 XML 中提取“谁锁了谁”的关键证据
死锁发生后,执行sp_readerrorlog查看最新错误日志,找到以<deadlock-list>开头的 XML 块。我们跳过冗长 XML,直取三个核心字段:
-- 读取最近错误日志(0=当前,1=上一个...) EXEC sp_readerrorlog 0, 1, 'deadlock';关键字段提取口诀(对照1222输出):
victim=processXXXXX:死锁牺牲者(被 Kill 的 SPID),如victim=process5e27708→ 对应 SPID 60;waitresource=KEY: 8:72057594066108416 (ef8a9edf5a1e):等待的资源,KEY:表示键级别锁,72057594066108416是hobtid(Heap or B-tree ID),即索引物理标识;lockMode=X/lockMode=U:锁模式,X=排他锁,U=更新锁(升级为 X 前的中间态);executionStack:SQL 执行栈,确认确实是UPDATE tt ... WHERE a=...在执行。
实战技巧:
hobtid数字看不懂?用以下脚本秒查对应索引名:SELECT o.name AS table_name, i.name AS index_name, i.type_desc AS index_type FROM sys.partitions p JOIN sys.indexes i ON p.object_id = i.object_id AND p.index_id = i.index_id JOIN sys.objects o ON i.object_id = o.object_id WHERE p.partition_id = 72057594066108416; -- 替换为你看到的 hobtid输出示例:
table_name=tt,index_name=ix_a_bc,index_type=NONCLUSTERED→ 锁发生在ix_a_bc索引上。
4. 执行计划深度剖析:三组实验的SET STATISTICS PROFILE ON对比揭示锁分裂根源
死锁的表象是锁等待,根因是执行计划。1222告诉你“谁锁了谁”,而执行计划告诉你“为什么需要锁这些资源”。我们用SET STATISTICS PROFILE ON获取带实际行数的执行计划(文字版),对比三组实验,答案一目了然。
4.1 获取执行计划:STATISTICS PROFILE比EXPLAIN更贴近真实执行
在 SSMS 中,对UPDATE语句前加SET STATISTICS PROFILE ON,执行后结果网格下方会出现一个“执行计划”结果集,包含Rows(实际影响行数)、Executes(执行次数)、EstimateRows(预估行数)等列。这是诊断锁行为的黄金数据。
-- 在实验组1(问题组)中执行: SET STATISTICS PROFILE ON; UPDATE tt WITH(ROWLOCK) SET d = 'cd' WHERE a = 'EF211985-EA72-4A40-81DA-0AAB076E7AA3'; SET STATISTICS PROFILE OFF;参数说明:
STATISTICS PROFILE开启后,SQL Server 在执行时收集每个操作符的实际性能数据(如 IO、CPU、行数),比SHOWPLAN_TEXT更真实。WITH(ROWLOCK)提示在此处有效,确保我们看到的是行锁行为,而非页锁或表锁。
4.2 三组执行计划关键对比:锁定操作符数量决定死锁概率
下表提炼三组实验中UPDATE语句执行计划的核心操作符(Operator)及其数量。操作符数量 = 锁申请次数的理论下限。
| 实验组 | d字段类型 | INCLUDE(d) | 执行计划关键操作符 | 操作符数量 | 锁申请阶段推断 |
|---|---|---|---|---|---|
| 组1(问题) | VARCHAR(MAX) | ✅ 有 | Index Update(ix_a_bc),Index Update(PK),Index Update(ix_b_cd) | 3个 | 分三阶段:先锁ix_a_bc找行 → 再锁PK改主键页 → 最后锁ix_b_cd改索引页 |
| 组2(精简) | VARCHAR(MAX) | ❌ 无 | Index Update(PK) | 1个 | 仅锁PK:因d不在索引中,UPDATE必须回表,故只更新聚集索引 |
| 组3(瘦身) | VARCHAR(200) | ✅ 有 | Index Update(ix_a_bc, PK, ix_b_cd) ——单操作符,三对象 | 1个 | 一步到位:d字段变小后,SQL Server 选择“合并更新”,在单个Index Update操作中同时刷三个索引页 |
技术深挖:为什么
VARCHAR(MAX)+INCLUDE会导致三阶段更新?因为MAX类型数据可能存储在行外(LOB Storage),SQL Server 无法保证在单次 I/O 中原子更新所有相关页。优化器被迫拆解为多个独立Index Update操作,每个操作申请自己的锁,形成锁申请序列。而VARCHAR(200)数据总在行内,且大小可控,优化器判断可安全合并,故生成单操作符计划。
4.3 锁序列还原:从 ProfilerLock:Acquired事件反推执行步骤
回到 Profiler 捕获的Lock:Acquired事件列表,按StartTime排序,我们能清晰看到组1的锁申请序列(简化版):
| 时间戳 | SPID | ObjectID2 (hobtid) | Mode | 对应索引 | 阶段 |
|---|---|---|---|---|---|
| T1 | 54 | 72057594066108416 | U | ix_a_bc | 阶段1:用a值查找目标行,申请更新锁 |
| T2 | 54 | 72057594065518592 | U | PK__tt__... | 阶段1:找到行后,对主键页申请更新锁 |
| T3 | 54 | 72057594065518592 | X | PK__tt__... | 阶段2:将主键页更新锁升级为排他锁,写入新d值 |
| T4 | 54 | 72057594066108416 | X | ix_a_bc | 阶段3:更新ix_a_bc索引页的d副本 |
| T5 | 54 | 72057594066173952 | X | ix_b_cd | 阶段3:更新ix_b_cd索引页的d副本 |
死锁发生点:SPID 60 在 T2 时刻申请
PK的U锁时,发现 SPID 54 已持有ix_a_bc的U锁(T1),于是等待;SPID 54 在 T4 时刻申请ix_a_bc的X锁时,发现 SPID 60 正在等待PK的U锁(T2),而PK的U锁又被 SPID 60 持有 → 形成循环等待。这就是经典的“锁升级时机错位”:SPID 54 释放了ix_a_bc的U锁(T3 后),但 SPID 60 还没来得及申请ix_a_bc的X锁,SPID 54 就抢先申请了。
5. 避坑与常见问题排查:5 条血泪经验,专治 SQL Server 死锁玄学
这个死锁案例之所以“奇怪”,是因为它违反直觉:同样的语句、同样的索引、同样的提示,只因一个字段类型或INCLUDE选项就天壤之别。以下是我在某跨平台系统迁移项目中踩过的坑,每一条都附带真实现象、根因和可立即执行的解决命令。
5.1 现象:死锁日志显示victim是UPDATE,但 Profiler 里找不到对应 SPID 的Lock:Acquired事件
原因:1222日志记录的是死锁发生瞬间的状态,而 Profiler 可能因缓冲区满或过滤条件过严丢失早期事件。更常见的是,你启动 Profiler 的时间晚于死锁发生时间。
解决:
- 启动 Profiler 后,立刻执行
DBCC SQLPERF('sys.dm_os_wait_stats', CLEAR)清空等待统计,再开始监听; - 在
Column Filters中,SPID过滤改为Not In→1,2,3,4(排除系统 SPID),避免漏掉动态分配的连接; - 使用
Extended Events替代 Profiler(SQL Server 2012+),创建xml_deadlock_report事件会话,它比 Profiler 更轻量、更可靠。
5.2 现象:SET STATISTICS PROFILE ON显示Index Update操作符只有 1 个,但死锁依然发生
原因:你正在查看的执行计划是SELECT或INSERT的,不是UPDATE的!UPDATE语句的执行计划必须单独捕获,且不能加任何OPTION (RECOMPILE)等提示干扰。
解决:
- 确保
SET STATISTICS PROFILE ON紧贴UPDATE语句前,且中间无空行或注释; - 执行后,在 SSMS 结果网格中切换到“执行计划”标签页(不是“消息”或“结果”),该页才是真实执行计划;
- 若仍看不到,检查数据库兼容级别:
SELECT compatibility_level FROM sys.databases WHERE name = 'YourDB',低于 100(SQL Server 2008)可能不支持完整 Profile。
5.3 现象:varchar(max)改成varchar(8000)后死锁消失,但改成varchar(4000)却依然死锁
原因:SQL Server 对varchar(n)的存储策略有临界点。n <= 8000时,数据默认存于数据页内(In-Row);n > 8000或n = MAX时,数据存于 LOB 页(Out-of-Row)。4000虽小于8000,但若表中已有大量d字段接近4000长度,SQL Server 可能因页空间不足而强制将其存入 LOB 页,触发与MAX相同的锁分裂逻辑。
解决:
- 执行
DBCC IND('YourDB', 'tt', -1)查看tt表的页分配情况,重点关注PageType = 3(数据页)和PageType = 10(LOB 页)的比例; - 若
PageType = 10占比高,说明d字段实际已 Out-of-Row,此时需ALTER TABLE tt REBUILD强制重组页,或直接ALTER COLUMN d VARCHAR(200)彻底规避。
5.4 现象:INCLUDE移除后死锁消失,但业务要求必须INCLUDE某些字段以支撑高频SELECT
原因:INCLUDE是双刃剑。它加速SELECT,却加重UPDATE/INSERT/DELETE的锁负担。当INCLUDE字段是MAX类型时,矛盾被放大。
解决:
- 方案A(推荐):将
MAX字段分离到独立表,主表只存d_id外键,INCLUDE改为d_id(INT); - 方案B:用计算列替代
INCLUDE,如ALTER TABLE tt ADD d_hash AS HASHBYTES('SHA2_256', d),INCLUDE(d_hash),SELECT时用哈希比对; - 方案C(应急):在
UPDATE语句中显式HOLDLOCK,强制锁升级顺序:UPDATE tt WITH(ROWLOCK, HOLDLOCK) SET d='cd' WHERE a='...'。
5.5 现象:在 SQL Server 2019 上复现失败,1222日志无死锁记录
原因:SQL Server 2016+ 引入了自适应查询处理(Adaptive Query Processing),其中Batch Mode on Rowstore和Interleaved Execution可能改变UPDATE计划。更重要的是,2019 默认启用Parameter Sniffing优化,若a值选择性差,可能走扫描而非查找,破坏死锁条件。
解决:
- 关闭自适应功能(临时):
ALTER DATABASE SCOPED CONFIGURATION SET DISABLE_BATCH_MODE = ON;; - 强制使用老式优化器:
UPDATE tt WITH(ROWLOCK) SET d='cd' WHERE a='...' OPTION (QUERYTRACEON 9481)(启用 Legacy CE); - 确保
a字段有高选择性:UPDATE STATISTICS tt ix_a_bc WITH FULLSCAN更新统计信息。
6. 进阶验证与生产防护:用sys.dm_exec_query_plan自动化检测高风险执行计划
死锁复现是学习手段,生产防护才是终极目标。与其等死锁报警再救火,不如在代码上线前,就用 DMV 扫描出所有潜在的“多索引更新”高危UPDATE语句。下面这套方法,我已在某高校教务系统中落地,覆盖 200+ 张核心表,将死锁发生率从月均 3 次降至 0。
6.1 构建高风险执行计划识别脚本:自动标记Index Update数量 ≥2 的语句
核心思路:从sys.dm_exec_cached_plans中提取所有缓存的UPDATE计划,解析其 XML,统计<RelOp>标签下PhysicalOp="Index Update"的出现次数。超过 1 次即为高风险。
-- 检测当前缓存中所有 UPDATE 语句的 Index Update 操作符数量 WITH xmlplans AS ( SELECT cp.plan_handle, qp.query_plan, cp.usecounts, cp.size_in_bytes, t.text AS sql_text FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) qp CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) t WHERE cp.cacheobjtype = 'Compiled Plan' AND t.text LIKE '%UPDATE%[ ]%' -- 粗略匹配 UPDATE 语句 AND qp.query_plan IS NOT NULL ), update_ops AS ( SELECT plan_handle, usecounts, size_in_bytes, sql_text, query_plan.value('count(//RelOp[@PhysicalOp="Index Update"])', 'int') AS index_update_count FROM xmlplans ) SELECT usecounts AS execution_count, size_in_bytes / 1024 AS plan_size_kb, LEFT(sql_text, 100) AS truncated_sql, index_update_count, CASE WHEN index_update_count >= 2 THEN 'HIGH RISK: Multiple Index Updates' ELSE 'LOW RISK' END AS risk_level FROM update_ops WHERE index_update_count >= 2 ORDER BY execution_count DESC;参数说明:
query_plan.value('count(//RelOp[@PhysicalOp="Index Update"])', 'int')是 XPath 表达式,精准统计 XML 中Index Update操作符数量。LEFT(sql_text, 100)截取 SQL 片段便于快速定位。此脚本可在任意时间运行,无需停机。
6.2 生产环境防护三板斧:从设计、监控到应急
第一板斧:设计规范前置
在数据库设计文档中强制加入“索引INCLUDE字段禁令”:
- 禁止对
VARCHAR(MAX),NVARCHAR(MAX),VARBINARY(MAX)字段使用INCLUDE; INCLUDE字段总长度(SUM(LEN))不得超过 900 字节(SQL Server 索引键长度限制);- 新增
INCLUDE前,必须用SET STATISTICS PROFILE ON验证UPDATE计划。
第二板斧:监控告警自动化
在 SQL Server Agent 中创建作业,每小时执行一次死锁检测脚本,并将结果写入监控表:
-- 创建死锁监控表 CREATE TABLE dbo.DeadlockMonitor ( id INT IDENTITY PRIMARY KEY, capture_time DATETIME2 DEFAULT GETDATE(), victim_spid INT, wait_resource NVARCHAR(256), lock_mode CHAR(1), sql_text NVARCHAR(500) ); -- 插入最近一次死锁信息(从 errorlog 解析,此处略去解析逻辑) INSERT INTO dbo.DeadlockMonitor (victim_spid, wait_resource, lock_mode, sql_text) SELECT CAST(x.value('(//deadlock/victim)[1]', 'varchar(10)') AS INT), x.value('(//resource-list/keylock/@waitresource)[1]', 'nvarchar(256)'), x.value('(//resource-list/keylock/@mode)[1]', 'char(1)'), x.value('(//executionStack/frame/@stmtstart)[1]', 'nvarchar(500)') FROM ( SELECT CAST(logtext AS XML) AS deadlock_xml FROM sys.fn_trace_gettable( (SELECT path FROM sys.traces WHERE is_default = 1), DEFAULT ) WHERE EventClass = 148 -- Deadlock Graph ) AS t CROSS APPLY deadlock_xml.nodes('/deadlock') AS T2(x);第三板斧:应急响应 SOP
当告警触发,立即执行:
DBCC INPUTBUFFER(@victim_spid)查看受害者最后执行的 SQL;SELECT * FROM sys.dm_exec_requests WHERE session_id = @victim_spid查看阻塞链;KILL @blocking_spid终止源头(谨慎!先确认非关键业务);ALTER INDEX ALL ON tt REBUILD WITH (ONLINE = ON)重建索引,释放碎片化锁资源。
从那以后我每次上线新索引,都强制走一遍SET STATISTICS PROFILE ON+UPDATE测试,哪怕只是加一个INCLUDE。不是 paranoid,而是这个死锁太狡猾——它不报错,不超时,就安静地把两个事务互相锁死,直到LOCK_TIMEOUT到期。希望帮到你。
本文还有配套的精品资源,点击获取