SQL Server阻塞与死锁排查实战:从DMV到根因分析
系列文章: SQL Server性能优化实战系列 · 第2篇
发布日期: 2026-09-16
字数: 约4900字
阅读时间: 15分钟
引言
在SQL Server性能问题中,阻塞与死锁是最让DBA头疼的场景之一。当用户抱怨"系统卡住了"、"查询一直在转圈"时,80%的情况下都与阻塞有关。与CPU、内存、I/O等资源瓶颈不同,阻塞问题的特征是:查询耗时长,但资源消耗低------因为被阻塞的会话在等待时不消耗CPU、内存或I/O资源。
本文将系统介绍SQL Server阻塞与锁的诊断方法,涵盖:
- 阻塞的本质与等待类型分析
- 锁定粒度与锁升级机制
- 长时间阻塞的识别与监控
- 实战DMV查询脚本
- 优化建议与最佳实践
本文所有脚本均在SQL Server 2016/2019/2022环境下测试通过,但在生产环境使用前请先在测试环境验证。
一、阻塞基础:理解锁与等待
1.1 什么是阻塞?
阻塞主要是对逻辑锁的等待 ,例如等待获取资源上的排他锁(X锁),或由较低级别同步原语(如闩锁)导致的等待。当请求获取已被锁定资源上的不兼容锁时,就会发生逻辑锁等待。
阻塞的特征:
- 查询耗时长(用户感知慢)
- 资源消耗低(CPU/内存/I/O都很低)
- 本质:等待其他会话释放锁
阻塞 vs 死锁:
- 阻塞:会话A等待会话B释放锁,最终会话B完成后,会话A可以继续执行
- 死锁:会话A等待会话B,会话B也等待会话A,形成循环等待,SQL Server会选择一个会话作为"死锁牺牲品"终止
1.2 SQL Server的等待机制
SQL Server会话在系统资源(或锁)当前不可用时被置于等待状态 。SQL Server提供了详细的等待信息,报告约数百种等待类型。
核心DMV:
sys.dm_os_wait_stats:整体和累积等待统计sys.dm_os_waiting_tasks:当前等待的会话(按会话细分)sys.dm_tran_locks:锁状态(已授予/等待中)sys.dm_exec_requests:被阻塞的请求
二、实战诊断:使用DMV定位阻塞
2.1 查找阻塞链
脚本1:使用sys.dm_os_waiting_tasks查找阻塞链
sql
-- 查找当前所有被阻塞的会话
SELECT
waiting_task_address,
session_id AS blocked_session,
exec_context_id,
wait_duration_ms,
wait_type,
resource_address,
blocking_task_address,
blocking_session_id AS blocking_session,
blocking_exec_context_id,
resource_description
FROM sys.dm_os_waiting_tasks
WHERE blocking_session_id IS NOT NULL
ORDER BY wait_duration_ms DESC;
示例输出解读:
makefile
blocked_session: 56
blocking_session: 53
wait_type: LCK_M_S (等待共享锁)
wait_duration_ms: 1103500 (约18分钟)
结论: 会话56被会话53阻塞,等待共享锁,已等待18分钟。
2.2 分析锁状态
脚本2:使用sys.dm_tran_locks分析锁状态
sql
-- 查看指定数据库的所有锁
SELECT
request_session_id AS spid,
resource_type AS lock_type,
resource_database_id AS db_id,
CASE resource_type
WHEN 'OBJECT' THEN OBJECT_NAME(resource_associated_entity_id, resource_database_id)
WHEN 'DATABASE' THEN ' '
ELSE (SELECT OBJECT_NAME(object_id, resource_database_id)
FROM sys.partitions
WHERE hobt_id = resource_associated_entity_id)
END AS object_name,
resource_description AS resource_desc,
request_mode AS lock_mode,
request_status AS lock_status
FROM sys.dm_tran_locks
WHERE resource_database_id = DB_ID('YourDatabaseName')
ORDER BY request_session_id, resource_type;
关键列解读:
request_status:- GRANT = 锁已授予
- WAIT = 等待中(被阻塞)
request_mode:锁模式- S = 共享锁(SELECT)
- X = 排他锁(INSERT/UPDATE/DELETE)
- IX = 意向排他锁
- IS = 意向共享锁
- U = 更新锁
resource_type:资源类型- DATABASE = 数据库级锁
- OBJECT = 对象级锁(表、索引)
- PAGE = 页级锁
- KEY = 行级锁(索引键)
- RID = 行级锁(堆表行ID)
2.3 查看被阻塞的请求及SQL文本
脚本3:查看被阻塞请求的完整信息
sql
-- 查看所有被阻塞的请求及其SQL文本
SELECT
r.session_id AS blocked_spid,
r.blocking_session_id AS blocking_spid,
r.wait_type,
r.wait_time / 1000.0 AS wait_time_sec,
r.wait_resource,
r.command,
DB_NAME(r.database_id) AS database_name,
r.cpu_time,
r.logical_reads,
r.total_elapsed_time / 1000.0 AS elapsed_time_sec,
SUBSTRING(st.text, (r.statement_start_offset/2)+1,
((CASE r.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE r.statement_end_offset
END - r.statement_start_offset)/2) + 1) AS blocked_sql_text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) st
WHERE r.blocking_session_id > 0
ORDER BY r.wait_time DESC;
使用场景:
- 用户报告"系统卡住"时,立即运行此脚本
- 定位哪些SQL正在被阻塞
- 找出阻塞源头(blocking_spid)
2.4 综合阻塞诊断脚本
脚本4:阻塞链完整视图(推荐)
sql
-- 综合诊断:显示阻塞链、SQL文本、锁信息
WITH BlockingChain AS (
SELECT
r.session_id,
r.blocking_session_id,
r.wait_type,
r.wait_time / 1000.0 AS wait_sec,
r.wait_resource,
DB_NAME(r.database_id) AS db_name,
SUBSTRING(st.text, (r.statement_start_offset/2)+1,
((CASE r.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE r.statement_end_offset
END - r.statement_start_offset)/2) + 1) AS sql_text,
s.login_name,
s.host_name,
s.program_name,
r.status,
r.cpu_time,
r.logical_reads
FROM sys.dm_exec_requests r
INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) st
WHERE r.session_id <> @@SPID
)
SELECT
bc.session_id AS [被阻塞会话],
bc.blocking_session_id AS [阻塞源会话],
bc.wait_sec AS [等待秒数],
bc.wait_type AS [等待类型],
bc.wait_resource AS [等待资源],
bc.db_name AS [数据库],
bc.sql_text AS [被阻塞SQL],
blocker.sql_text AS [阻塞者SQL],
bc.login_name AS [被阻塞登录],
blocker.login_name AS [阻塞者登录],
bc.host_name AS [被阻塞主机],
bc.program_name AS [被阻塞程序]
FROM BlockingChain bc
LEFT JOIN BlockingChain blocker ON bc.blocking_session_id = blocker.session_id
WHERE bc.blocking_session_id > 0
ORDER BY bc.wait_sec DESC;
此脚本的价值:
- 一次查询即可看到完整阻塞链
- 同时显示被阻塞者和阻塞者的SQL
- 包含登录名、主机名、程序名(便于追溯应用)
三、锁定粒度与锁升级
3.1 事务隔离级别与锁持续时间
理解阻塞的关键之一是事务隔离级别和锁定粒度。
事务隔离级别对锁的影响:
- 事务隔离级别控制**共享锁(S锁)**的持续时间
- 但**不影响排他锁(X锁)**的持续时间
- 排他锁在所有隔离级别下都会持有到事务结束
四种隔离级别:
-
READ UNCOMMITTED(未提交读)
- 不加共享锁(脏读)
- 最低隔离级别,最高并发
-
READ COMMITTED(已提交读,默认)
- 读取时加共享锁
- 读取完成后立即释放(不持有到事务结束)
-
REPEATABLE READ(可重复读)
- 读取时加共享锁
- 持有到事务结束(保证可重复读)
-
SERIALIZABLE(可序列化)
- 读取时加范围锁
- 持有到事务结束(防止幻读)
3.2 锁定粒度:行级 vs 页级 vs 表级
锁定粒度的权衡:
- 行级锁:并发性高,但锁管理开销大
- 页级锁:折中方案
- 表级锁:并发性低,但锁管理开销小
SQL Server会根据情况自动升级锁,从行级锁→页级锁→表级锁。
3.3 锁升级触发条件
锁升级会在以下情况触发:
-
锁数量阈值:
- 单个语句在索引或堆上持有的锁数(包括意向锁)超过约5,000个
-
锁内存阈值:
- 锁资源占用的内存超过非AWE启用内存的40%
注意事项:
- 以下情况不会 触发锁升级:
- 事务在单个语句中在两个索引或堆上各获取2,500个锁(未达到单个对象5,000锁阈值)
- 事务在非聚集索引和对应基表上各获取2,500个锁(分别计算)
3.4 控制锁升级
SQL Server 2008+提供了表级锁升级控制(推荐):
sql
-- 禁用表的锁升级
ALTER TABLE TableName SET (LOCK_ESCALATION = DISABLE);
-- 启用分区级锁升级(仅升级到分区级别,而非表级)
ALTER TABLE TableName SET (LOCK_ESCALATION = AUTO);
-- 恢复默认行为(表级锁升级)
ALTER TABLE TableName SET (LOCK_ESCALATION = TABLE);
查看当前锁升级设置:
sql
SELECT
name AS table_name,
lock_escalation_desc
FROM sys.tables
WHERE lock_escalation_desc <> 'TABLE'
ORDER BY name;
全局跟踪标记(仍可用但不推荐):
- 跟踪标记1211:完全禁用锁升级(可能导致锁内存耗尽)
- 跟踪标记1224:仅在锁内存达到40%阈值时才允许锁升级
四、识别长时间阻塞
4.1 配置阻塞进程阈值
SQL Server允许设置服务器级阻塞阈值,任何超过此阈值的阻塞将触发可捕获的事件。
配置阻塞阈值(单位:秒):
sql
-- 设置阻塞阈值为20秒
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'blocked process threshold', 20;
RECONFIGURE;
解读:
- 任何阻塞超过20秒的会话都会触发
blocked_process_report事件 - 可通过扩展事件或SQL Profiler捕获
4.2 使用扩展事件捕获阻塞报告
推荐方法(开销低于SQL Profiler):
sql
-- 创建扩展事件会话
CREATE EVENT SESSION BlockedProcessReport ON SERVER
ADD EVENT sqlserver.blocked_process_report
ADD TARGET package0.ring_buffer
WITH (
MAX_MEMORY = 4096 KB,
EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS
);
-- 启动会话
ALTER EVENT SESSION BlockedProcessReport ON SERVER STATE = START;
查询捕获的阻塞事件:
sql
SELECT
event_data.value('(event/@timestamp)[1]', 'datetime2') AS event_time,
event_data.value('(event/data[@name="duration"]/value)[1]', 'bigint') / 1000 AS duration_sec,
event_data.value('(event/data[@name="database_name"]/value)[1]', 'nvarchar(128)') AS database_name,
event_data.value('(event/data[@name="blocked_process"]/value)[1]', 'nvarchar(max)') AS blocked_process_report
FROM (
SELECT CONVERT(XML, event_data) AS event_data
FROM sys.dm_xe_session_targets t
JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address
WHERE s.name = 'BlockedProcessReport'
AND t.target_name = 'ring_buffer'
) AS x
ORDER BY event_time DESC;
五、对象级阻塞分析
5.1 使用sys.dm_db_index_operational_stats
sys.dm_db_index_operational_stats提供全面的索引使用统计信息,包括按表、索引和分区划分的详细锁定统计。
脚本5:分析对象级锁等待
sql
-- 查找锁等待最严重的表和索引
SELECT
DB_NAME(ios.database_id) AS database_name,
OBJECT_NAME(ios.object_id, ios.database_id) AS table_name,
i.name AS index_name,
ios.row_lock_count AS [行锁次数],
ios.row_lock_wait_count AS [行锁等待次数],
ios.row_lock_wait_in_ms AS [行锁等待毫秒],
CASE WHEN ios.row_lock_count > 0
THEN ios.row_lock_wait_in_ms * 1.0 / ios.row_lock_count
ELSE 0 END AS [平均行锁等待ms],
ios.page_lock_count AS [页锁次数],
ios.page_lock_wait_count AS [页锁等待次数],
ios.page_lock_wait_in_ms AS [页锁等待毫秒],
ios.page_latch_wait_count AS [页闩锁等待次数],
ios.page_latch_wait_in_ms AS [页闩锁等待毫秒],
ios.page_io_latch_wait_count AS [页IO闩锁等待次数],
ios.page_io_latch_wait_in_ms AS [页IO闩锁等待毫秒]
FROM sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL) ios
JOIN sys.indexes i ON ios.object_id = i.object_id AND ios.index_id = i.index_id
WHERE ios.row_lock_wait_in_ms > 0
OR ios.page_lock_wait_in_ms > 0
ORDER BY (ios.row_lock_wait_in_ms + ios.page_lock_wait_in_ms) DESC;
关键指标解读:
| 指标 | 说明 |
|---|---|
row_lock_count |
持有的行锁数量 |
row_lock_wait_count |
等待行锁的次数 |
row_lock_wait_in_ms |
等待行锁的总毫秒数 |
page_latch_wait_count |
页闩锁等待次数(例如热点页面争用) |
page_io_latch_wait_count |
页I/O闩锁等待次数(慢速I/O导致) |
使用场景:
- 识别哪些表/索引是阻塞的热点
- 找出递增键插入导致的热点页面争用
- 分析是否需要调整索引设计
注意: 此DMV的信息从实例启动开始累积,实例重启后会丢失。建议定期轮询并保存到历史表。
六、整体阻塞影响评估
6.1 使用sys.dm_os_wait_stats
sys.dm_os_wait_stats汇总了所有连接的等待信息,可按等待类型分类以获取给定工作负载的性能概况。
脚本6:查询累计等待最高的等待类型
sql
-- 排除无关的等待类型,聚焦业务相关等待
SELECT TOP 20
wait_type AS [等待类型],
waiting_tasks_count AS [等待任务数],
wait_time_ms AS [总等待毫秒],
max_wait_time_ms AS [最大单次等待ms],
signal_wait_time_ms AS [信号等待ms],
wait_time_ms - signal_wait_time_ms AS [资源等待ms],
CAST((wait_time_ms - signal_wait_time_ms) * 100.0 / SUM(wait_time_ms) OVER() AS DECIMAL(5,2)) AS [占比%]
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
-- 排除后台任务、空闲等待等无关等待类型
'BROKER_EVENTHANDLER', 'BROKER_RECEIVE_WAITFOR', 'BROKER_TASK_STOP',
'BROKER_TO_FLUSH', 'BROKER_TRANSMITTER', 'CHECKPOINT_QUEUE',
'CHKPT', 'CLR_AUTO_EVENT', 'CLR_MANUAL_EVENT', 'CLR_SEMAPHORE',
'DBMIRROR_DBM_EVENT', 'DBMIRROR_EVENTS_QUEUE', 'DBMIRROR_WORKER_QUEUE',
'DBMIRRORING_CMD', 'DIRTY_PAGE_POLL', 'DISPATCHER_QUEUE_SEMAPHORE',
'EXECSYNC', 'FSAGENT', 'FT_IFTS_SCHEDULER_IDLE_WAIT', 'FT_IFTSHC_MUTEX',
'HADR_CLUSAPI_CALL', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'HADR_LOGCAPTURE_WAIT',
'HADR_NOTIFICATION_DEQUEUE', 'HADR_TIMER_TASK', 'HADR_WORK_QUEUE',
'KSOURCE_WAKEUP', 'LAZYWRITER_SLEEP', 'LOGMGR_QUEUE',
'ONDEMAND_TASK_QUEUE', 'PREEMPTIVE_XE_GETTARGETSTATE',
'PWAIT_ALL_COMPONENTS_INITIALIZED', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP',
'QDS_ASYNC_QUEUE', 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP',
'REQUEST_FOR_DEADLOCK_SEARCH', 'RESOURCE_QUEUE', 'SERVER_IDLE_CHECK',
'SLEEP_BPOOL_FLUSH', 'SLEEP_DBSTARTUP', 'SLEEP_SYSTEMTASK',
'SLEEP_TASK', 'SLEEP_TEMPDBSTARTUP', 'SNI_HTTP_ACCEPT',
'SQLTRACE_BUFFER_FLUSH', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP',
'WAITFOR', 'WAITFOR_TASKSHUTDOWN', 'WAIT_FOR_RESULTS',
'XE_DISPATCHER_JOIN', 'XE_DISPATCHER_WAIT', 'XE_TIMER_EVENT'
)
AND wait_time_ms > 0
ORDER BY wait_time_ms DESC;
关键指标解读:
| 指标 | 说明 |
|---|---|
wait_time_ms |
总等待时间(包含资源等待+信号等待) |
signal_wait_time_ms |
从资源可用到线程在CPU上调度的等待时间 高值表示CPU争用 |
resource_wait_time_ms |
wait_time_ms - signal_wait_time_ms 实际等待资源的时间 |
常见的锁相关等待类型:
LCK_M_S:等待共享锁(SELECT被阻塞)LCK_M_X:等待排他锁(UPDATE/DELETE被阻塞)LCK_M_U:等待更新锁LCK_M_IX:等待意向排他锁LCK_M_IS:等待意向共享锁
重置等待统计(用于基准测试):
sql
DBCC SQLPERF('sys.dm_os_wait_stats', CLEAR);
注意: 重置会清空所有历史数据,建议在维护窗口或测试环境使用。
七、优化建议与最佳实践
7.1 短期优化(立即可执行)
1. 识别并终止长时间阻塞的会话
sql
-- 杀掉长时间阻塞的会话(谨慎使用)
KILL <blocking_session_id>;
2. 调整事务隔离级别
sql
-- 对于读操作,考虑使用READ UNCOMMITTED(如果业务允许脏读)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT * FROM Orders WHERE OrderID = 12345;
3. 缩短事务持续时间
- 避免在事务中执行长时间操作(如调用外部API)
- 尽早提交或回滚事务
- 避免在事务中进行用户交互
7.2 中期优化(需要规划)
1. 优化索引设计
- 覆盖索引减少锁持有时间
- 合理的索引可以降低锁粒度(避免表级锁)
2. 使用行版本控制
sql
-- 启用SNAPSHOT隔离级别(避免读操作被阻塞)
ALTER DATABASE YourDatabase SET ALLOW_SNAPSHOT_ISOLATION ON;
ALTER DATABASE YourDatabase SET READ_COMMITTED_SNAPSHOT ON;
优势:
- 读操作不加锁(使用行版本)
- 读操作不被写操作阻塞
- 适合读多写少的场景
劣势:
- 增加tempdb负载(存储行版本)
- 可能导致更新冲突
3. 分区表(降低锁争用)
- 将大表分区,不同分区可以并行访问
- 启用分区级锁升级
7.3 长期优化(架构层面)
1. 应用程序设计
- 使用乐观并发控制(而不是悲观锁)
- 实现重试机制(处理死锁)
- 批量操作改为小批量(减少锁持有时间)
2. 读写分离
- 使用Always On只读副本
- 报表查询路由到只读副本
- 减少主库的读压力
3. 数据库设计
- 避免热点表(如序列号表、计数器表)
- 使用队列表代替直接更新
- 考虑使用In-Memory OLTP(内存优化表)
八、阻塞诊断流程图
下图展示了完整的阻塞问题诊断流程,从问题报告到优化实施的完整路径:

流程图说明:
这个流程图包含4个泳道 、10个节点 、2个分支路径 和3组详细的指导卡片:
四个泳道:
- 问题发现:开始诊断 → 检查系统资源 →(分支:资源瓶颈参考第1篇/第4篇)
- 阻塞分析:查找阻塞链 → 等待类型分析(LCK_M_S / LCK_M_X)
- SQL与事务:锁状态分析(GRANT/WAIT)→ 获取SQL文本
- 优化处理:检查事务时长(>30秒阈值)→ 优化实施 → 持续监控
三组指导卡片:
- 关键诊断指标:blocking_session_id、LCK等待类型、wait_duration_ms
- 锁与事务分析要点:锁粒度判断、锁状态、事务时长、锁升级阈值
- 优化策略三层次:短期(KILL)/中期(SQL优化)/长期(配置调整)/持续(监控)
九、阻塞问题排查清单
当用户报告"系统卡住"时,按以下顺序执行:
sql
第1步:快速定位阻塞链
→ 运行脚本1:sys.dm_os_waiting_tasks
→ 找出blocking_session_id
第2步:查看阻塞者和被阻塞者的SQL
→ 运行脚本4:综合阻塞诊断
→ 确认是什么SQL导致阻塞
第3步:分析锁状态
→ 运行脚本2:sys.dm_tran_locks
→ 确认锁类型(S/X/IX)和锁粒度(行/页/表)
第4步:评估影响范围
→ 运行脚本6:sys.dm_os_wait_stats
→ 确认阻塞是否是系统性问题
第5步:决策
→ 短期:KILL阻塞会话(如果必要)
→ 中期:优化SQL/索引/隔离级别
→ 长期:调整应用架构
十、总结
阻塞问题是SQL Server性能优化中最常见的场景之一。通过本文介绍的DMV查询和诊断方法,你可以:
- 快速定位阻塞链(谁阻塞了谁)
- 分析根因(锁类型、锁粒度、SQL文本)
- 评估影响(等待时间、影响范围)
- 采取行动(短期/中期/长期优化)
关键要点:
- 阻塞的特征:查询慢 + 资源消耗低
- 核心DMV:
sys.dm_os_waiting_tasks、sys.dm_tran_locks、sys.dm_exec_requests - 锁升级机制:5,000锁阈值或40%内存阈值
- 优化方向:缩短事务、优化索引、调整隔离级别、读写分离
本文提供的DMV脚本可以直接用于生产环境诊断,建议收藏备用。
下一篇预告: SQL Server内存管理深度剖析:从Buffer Pool到压力诊断
关于作者
Master Xu ,20+ 年 IT、通信、电信、医疗、制造行业经验,企业级数据库架构师。 擅长企业IT架构规划、数据库性能优化、大型复杂项目交付。
欢迎交流技术话题:
- 📧 Email: xcoolwinds@gmail.com
- 💬 微信: xcoolwinds
- 🔗 知乎: www.zhihu.com/people/da-f...
- 💻 CSDN: blog.csdn.net/xcoolwinds
开放合作: 技术咨询 | 架构顾问 | 企业内训 | 技术分享
系列文章预告
本文是"SQL Server性能优化实战系列"的第2篇,后续文章:
- 第3篇: SQL Server内存管理深度剖析:从Buffer Pool到压力诊断
- 第4篇: SQL Server I/O性能优化:从等待类型到存储配置
- 第5篇: SQL Server扩展事件(Extended Events)完全指南
- 第6篇: tempdb性能优化完全指南:空间管理与并发争用
敬请关注!
标签
#SQL Server #阻塞诊断 #死锁排查 #DMV #数据库调优 #DBA
话题(知乎)
#SQL Server #数据库性能优化 #DBA #后端开发 #故障排查
声明: 本文所有脚本均在SQL Server 2016/2019/2022环境下测试通过,但在生产环境使用前请先在测试环境验证。
版权: 本文为原创文章,转载请注明出处。
发布日期:2026-09-16