Skip to content

SQLServer 锁优化

锁性能概述

锁的性能影响

在 SQL Server 中,锁是保证数据一致性和并发控制的核心机制,但不当的锁使用会导致严重的性能问题。锁性能问题主要表现为:

  • 锁等待:事务等待获取锁而暂停执行
  • 阻塞:一个事务持有锁导致其他事务无法执行
  • 死锁:两个或多个事务互相等待对方释放锁
  • 锁升级:从行级锁升级为表级锁,导致并发度下降

常见锁性能问题

生产环境常见锁性能问题

  • 长事务持有锁时间过长,导致大量阻塞
  • 不合理的隔离级别导致过度锁定
  • 缺乏合适的索引,导致全表扫描和大范围锁定
  • 复杂查询生成低效执行计划,增加锁竞争
  • 死锁导致事务回滚,影响业务连续性

锁性能评估

生产环境锁性能评估方法

  • 监控锁等待时间和次数
  • 分析阻塞和死锁事件
  • 评估锁升级频率
  • 检查事务持续时间
  • 分析查询执行计划中的锁提示和警告

锁等待与阻塞

锁等待概述

锁等待是指一个事务需要获取某个资源的锁,但该资源已被其他事务锁定,导致当前事务暂停执行,等待锁释放。锁等待是正常的并发控制机制,但频繁或长时间的锁等待会严重影响系统性能。

阻塞概述

阻塞是指一个事务(阻塞者)持有锁,导致另一个事务(被阻塞者)无法继续执行。阻塞是锁等待的直接结果,严重的阻塞会导致大量事务排队,系统响应时间急剧下降。

死锁概述

死锁是指两个或多个事务互相等待对方释放锁,形成循环等待,导致所有相关事务无法继续执行。SQL Server 会自动检测死锁,并选择一个事务作为牺牲品进行回滚,以打破死锁。

查看锁等待与阻塞

生产环境实践

sql
-- 查看当前阻塞情况
SELECT 
    session_id AS BlockedSessionID,
    blocking_session_id AS BlockingSessionID,
    wait_type AS WaitType,
    wait_time AS WaitTimeMs,
    resource_description AS ResourceDescription
FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0;

-- 查看锁等待统计
SELECT 
    wait_type,
    wait_time_ms / 1000.0 AS TotalWaitTimeSeconds,
    waiting_tasks_count AS WaitCount,
    wait_time_ms / waiting_tasks_count AS AvgWaitTimeMs
FROM sys.dm_os_wait_stats
WHERE wait_type LIKE '%LOCK%' OR wait_type LIKE '%LATCH%'
ORDER BY wait_time_ms DESC;

-- 查看死锁信息(SQL Server 2012+)
SELECT 
    XEvent.query('.') AS XEventData
FROM (
    SELECT 
        CAST(target_data AS XML) AS TargetData
    FROM sys.dm_xe_session_targets st
    JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address
    WHERE s.name = 'system_health'
) AS Data
CROSS APPLY TargetData.nodes('//event[@name=''xml_deadlock_report'']') AS XEvent(XEvent);

锁优化策略

减少锁的持有时间

生产环境最佳实践

  • 缩短事务持续时间,尽快提交或回滚
  • 避免在事务中执行不必要的操作(如等待用户输入、调用外部服务)
  • 将查询优化为更快执行,减少锁持有时间
  • 使用异步处理模式,将长事务拆分为短事务

示例

sql
-- 优化前:长事务持有锁时间长
BEGIN TRANSACTION;
    -- 执行耗时操作
    UPDATE dbo.t_Order_Header SET Status = 'Shipped' WHERE OrderID = @OrderID;
    -- 执行外部API调用(耗时)
    EXEC dbo.sp_CallShippingAPI @OrderID;
COMMIT TRANSACTION;

-- 优化后:缩短事务持有时间
UPDATE dbo.t_Order_Header SET Status = 'Processing' WHERE OrderID = @OrderID;
-- 执行外部API调用(事务外)
EXEC dbo.sp_CallShippingAPI @OrderID;
-- 短事务更新最终状态
BEGIN TRANSACTION;
    UPDATE dbo.t_Order_Header SET Status = 'Shipped' WHERE OrderID = @OrderID;
COMMIT TRANSACTION;

降低锁的粒度

生产环境最佳实践

  • 使用行级锁而非表级锁,提高并发度
  • 创建合适的索引,减少锁定范围
  • 避免使用表级锁提示(如 TABLOCK)
  • 考虑使用分区表,将锁范围限制在分区内

使用合适的隔离级别

生产环境隔离级别选择

  • READ COMMITTED:默认隔离级别,适合大多数场景
  • READ COMMITTED SNAPSHOT:使用行版本控制,减少读-写阻塞
  • SNAPSHOT:完全使用行版本控制,消除读-写阻塞,但增加存储开销
  • REPEATABLE READ/SERIALIZABLE:仅在必要时使用,会增加锁竞争

示例

sql
-- 启用 READ_COMMITTED_SNAPSHOT 隔离级别
ALTER DATABASE [ECommerce] SET READ_COMMITTED_SNAPSHOT ON;

-- 或在会话级别设置
SET TRANSACTION ISOLATION LEVEL READ COMMITTED SNAPSHOT;

优化查询语句

生产环境查询优化

  • 避免 SELECT *,只查询需要的列
  • 使用 WHERE 子句限制返回行数
  • 优化 JOIN 条件,确保使用索引
  • 避免在 WHERE 子句中使用函数,导致索引失效
  • 合理使用 TOP 或 OFFSET-FETCH 限制结果集

索引与锁优化

索引设计对锁的影响

合理的索引设计可以显著减少锁的持有时间和范围:

  • 主键和唯一索引:提供精确的行定位,减少锁定范围
  • 非聚集索引:允许索引查找代替表扫描,减少锁竞争
  • 覆盖索引:避免键查找,减少锁的数量和持有时间

覆盖索引

覆盖索引包含查询所需的所有列,避免了键查找操作,从而减少了锁的持有时间和范围。

示例

sql
-- 查询需要 OrderID, CustomerID 和 OrderDate
SELECT OrderID, CustomerID, OrderDate
FROM dbo.t_Order_Header
WHERE CustomerID = @CustomerID AND OrderDate >= @StartDate;

-- 创建覆盖索引,包含所有查询列
CREATE INDEX IX_t_Order_Header_CustomerID_OrderDate
ON dbo.t_Order_Header(CustomerID, OrderDate)
INCLUDE (OrderID);

索引碎片管理

索引碎片会导致查询需要读取更多的数据页,增加锁持有时间和范围。定期维护索引可以减少碎片,提高查询性能。

生产环境实践

sql
-- 查看索引碎片
SELECT 
    OBJECT_NAME(ips.object_id) AS TableName,
    i.name AS IndexName,
    ips.avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'SAMPLED') ips
JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE ips.avg_fragmentation_in_percent > 10;

-- 重新组织或重建索引
ALTER INDEX IX_t_Order_Header_CustomerID_OrderDate ON dbo.t_Order_Header REORGANIZE;
-- 或
ALTER INDEX IX_t_Order_Header_CustomerID_OrderDate ON dbo.t_Order_Header REBUILD WITH (ONLINE = ON);

索引统计信息

过时的统计信息会导致查询优化器生成低效的执行计划,增加锁竞争。定期更新统计信息可以确保查询优化器做出正确的决策。

生产环境实践

sql
-- 更新单个表的统计信息
UPDATE STATISTICS dbo.t_Order_Header WITH FULLSCAN;

-- 更新所有表的统计信息
EXEC sp_updatestats;

查询优化与锁

查询计划优化

查询计划是影响锁性能的关键因素,低效的执行计划会导致不必要的锁和更长的锁持有时间。

生产环境优化方法

  • 查看查询执行计划,识别扫描操作和警告
  • 使用索引提示(谨慎使用)
  • 更新统计信息,确保执行计划准确
  • 考虑使用查询存储(SQL Server 2016+)监控和修复执行计划

避免全表扫描

全表扫描会锁定整个表或大量行,严重影响并发性能。避免全表扫描的方法:

  • 创建合适的索引
  • 使用 WHERE 子句限制返回行数
  • 避免在 WHERE 子句中使用函数
  • 合理使用索引覆盖

优化 JOIN 操作

JOIN 操作是锁竞争的常见来源,优化 JOIN 操作的方法:

  • 使用 INNER JOIN 代替 OUTER JOIN,减少锁定范围
  • 确保 JOIN 条件使用索引列
  • 限制 JOIN 表的数量和大小
  • 考虑使用临时表或 CTE 分解复杂 JOIN

减少排序操作

排序操作会增加查询执行时间和锁持有时间,优化方法:

  • 创建索引,利用索引顺序避免排序
  • 减少 ORDER BY 子句中的列数
  • 考虑使用分页查询,减少排序数据量
  • 优化查询,减少需要排序的行数

事务优化与锁

缩短事务时间

长事务是锁性能问题的主要原因之一,缩短事务时间的方法:

  • 将大事务拆分为多个小事务
  • 避免在事务中执行非数据库操作
  • 优化事务中的查询,减少执行时间
  • 使用异步处理模式

避免长事务

生产环境长事务识别

sql
-- 查找长事务
SELECT 
    session_id,
    open_transaction_count,
    total_elapsed_time / 1000.0 AS ElapsedSeconds,
    text AS QueryText
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle)
WHERE open_transaction_count > 0
ORDER BY total_elapsed_time DESC;

合理使用事务隔离级别

不同的隔离级别对锁性能有显著影响,选择合适的隔离级别的原则:

  • 优先使用 READ COMMITTED 或 READ COMMITTED SNAPSHOT
  • 避免使用 REPEATABLE READ 和 SERIALIZABLE,除非必要
  • 考虑使用 SNAPSHOT 隔离级别消除读-写阻塞

事务拆分

将大事务拆分为多个小事务可以减少锁持有时间和范围:

示例

sql
-- 优化前:单个大事务更新大量行
BEGIN TRANSACTION;
    UPDATE dbo.t_Order_Line
    SET Status = 'Shipped'
    WHERE OrderID IN (SELECT OrderID FROM dbo.t_Order_Header WHERE ShipDate <= GETDATE());
COMMIT TRANSACTION;

-- 优化后:批量更新,拆分为多个小事务
DECLARE @BatchSize INT = 1000;
DECLARE @StartID INT = 0;
DECLARE @MaxID INT;

SELECT @MaxID = MAX(OrderLineID) FROM dbo.t_Order_Line;

WHILE @StartID < @MaxID
BEGIN
    BEGIN TRANSACTION;
        UPDATE dbo.t_Order_Line
        SET Status = 'Shipped'
        WHERE OrderLineID BETWEEN @StartID AND @StartID + @BatchSize
        AND OrderID IN (SELECT OrderID FROM dbo.t_Order_Header WHERE ShipDate <= GETDATE());
    COMMIT TRANSACTION;
    
    SET @StartID = @StartID + @BatchSize + 1;
    -- 可选:添加延迟,减少系统负载
    WAITFOR DELAY '00:00:00.1';
END;

系统配置优化

内存配置

足够的内存可以减少磁盘 I/O,缩短查询执行时间,从而减少锁持有时间。

生产环境实践

sql
-- 设置最大内存为 64GB
EXEC sp_configure 'max server memory (MB)', 65536;
RECONFIGURE;

并行度配置

合理的并行度配置可以提高查询性能,减少锁持有时间,但过度并行会导致更多的锁竞争。

生产环境实践

sql
-- 设置最大并行度为 CPU 核心数或 8(取较小值)
EXEC sp_configure 'max degree of parallelism', 4;
RECONFIGURE;

TempDB 配置

TempDB 性能问题会导致查询执行时间增加,从而延长锁持有时间。优化 TempDB 配置可以提高查询性能。

生产环境实践

  • 创建多个 TempDB 数据文件,数量建议为 CPU 核心数或 8(取较小值)
  • 所有 TempDB 数据文件大小相同
  • 将 TempDB 放在高速存储上
  • 设置合适的初始大小,避免自动增长

最大并发连接数

过多的并发连接会导致锁竞争加剧,合理设置最大并发连接数可以平衡系统负载。

生产环境实践

sql
-- 查看当前连接数
SELECT COUNT(*) AS CurrentConnections FROM sys.dm_exec_sessions WHERE is_user_process = 1;

-- 设置最大并发连接数(如需调整)
EXEC sp_configure 'user connections', 1000;
RECONFIGURE;

锁监控与分析

动态管理视图 (DMVs)

动态管理视图提供了丰富的锁相关信息,是监控和分析锁性能的主要工具。

生产环境常用 DMVs

  • sys.dm_tran_locks:当前锁信息
  • sys.dm_exec_requests:当前请求和等待信息
  • sys.dm_os_wait_stats:等待统计信息
  • sys.dm_os_waiting_tasks:当前等待任务
  • sys.dm_tran_active_transactions:活动事务信息

扩展事件

扩展事件是 SQL Server 提供的轻量级事件跟踪系统,适合生产环境监控锁相关事件。

生产环境实践

sql
-- 创建扩展事件会话监控锁等待
CREATE EVENT SESSION [LockWaits] ON SERVER 
ADD EVENT sqlserver.lock_acquired(
    ACTION(sqlserver.client_app_name,sqlserver.client_process_id,sqlserver.database_name,sqlserver.nt_username,sqlserver.sql_text)
    WHERE ([duration]>(1000000))), -- 等待超过 1 秒
ADD EVENT sqlserver.lock_released(
    ACTION(sqlserver.client_app_name,sqlserver.client_process_id,sqlserver.database_name,sqlserver.nt_username,sqlserver.sql_text)
    WHERE ([duration]>(1000000))),
ADD EVENT sqlserver.lock_timeout(
    ACTION(sqlserver.client_app_name,sqlserver.client_process_id,sqlserver.database_name,sqlserver.nt_username,sqlserver.sql_text)),
ADD EVENT sqlserver.xml_deadlock_report(
    ACTION(sqlserver.client_app_name,sqlserver.client_process_id,sqlserver.database_name,sqlserver.nt_username))
ADD TARGET package0.event_file(SET filename=N'D:\XEvents\LockWaits.xel',max_file_size=(50),max_rollover_files=(5))
WITH (MAX_MEMORY=4096 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,MAX_DISPATCH_LATENCY=30 SECONDS,MAX_EVENT_SIZE=0 KB,MEMORY_PARTITION_MODE=NONE,TRACK_CAUSALITY=OFF,STARTUP_STATE=OFF);

SQL Server Profiler

SQL Server Profiler 可以捕获和分析锁相关事件,但由于性能开销较大,建议仅在测试环境或短时间生产环境分析中使用。

锁等待统计

锁等待统计可以帮助识别系统中最常见的锁等待类型,从而针对性地进行优化。

生产环境实践

sql
-- 分析锁等待统计
SELECT 
    wait_type,
    wait_time_ms / 1000.0 AS TotalWaitTimeSeconds,
    waiting_tasks_count AS WaitCount,
    wait_time_ms / waiting_tasks_count AS AvgWaitTimeMs,
    CASE wait_type
        WHEN 'LCK_M_S' THEN '共享锁等待'
        WHEN 'LCK_M_X' THEN '排他锁等待'
        WHEN 'LCK_M_IS' THEN '意向共享锁等待'
        WHEN 'LCK_M_IX' THEN '意向排他锁等待'
        WHEN 'LCK_M_SIX' THEN '意向排他共享锁等待'
        WHEN 'LCK_M_U' THEN '更新锁等待'
        WHEN 'LCK_M_SCH_S' THEN '架构共享锁等待'
        WHEN 'LCK_M_SCH_M' THEN '架构修改锁等待'
        WHEN 'LCK_M_BU' THEN '大容量更新锁等待'
        ELSE wait_type
    END AS WaitTypeDescription
FROM sys.dm_os_wait_stats
WHERE wait_type LIKE 'LCK_%'
ORDER BY wait_time_ms DESC;

死锁管理

死锁检测

SQL Server 会自动检测死锁,默认检测间隔为 500ms。可以通过以下方法监控死锁:

  • 查看系统健康会话中的死锁报告
  • 使用扩展事件捕获死锁事件
  • 监控 SQL Server 错误日志中的死锁信息

死锁分析

死锁分析的关键是查看死锁图,识别导致死锁的事务和资源。死锁图包含以下信息:

  • 参与死锁的事务
  • 锁定的资源
  • 事务执行的 SQL 语句
  • 死锁牺牲品

死锁避免

生产环境死锁避免策略

  • 保持事务简短,减少锁持有时间
  • 使用相同的访问顺序访问资源
  • 避免在事务中执行用户交互
  • 使用较低的隔离级别
  • 添加适当的索引,减少锁竞争
  • 考虑使用乐观并发控制

死锁解决

生产环境死锁解决方法

  • 分析死锁图,识别根本原因
  • 优化导致死锁的查询和事务
  • 添加适当的索引
  • 调整隔离级别
  • 考虑使用应用程序级别的重试机制

锁优化最佳实践

设计阶段优化

  • 数据库设计:合理设计表结构和关系,减少复杂查询
  • 索引设计:创建合适的索引,支持常用查询
  • 分区设计:使用分区表减少锁竞争
  • 隔离级别选择:根据业务需求选择合适的隔离级别

开发阶段优化

  • 查询优化:编写高效的 SQL 查询,避免全表扫描
  • 事务管理:使用短事务,避免长事务
  • 锁提示:谨慎使用锁提示,避免过度锁定
  • 并发控制:考虑使用乐观并发控制
  • 代码审查:定期审查代码中的锁相关问题

运维阶段优化

  • 监控锁性能:定期监控锁等待、阻塞和死锁
  • 索引维护:定期重建或重新组织索引,更新统计信息
  • 系统配置:优化内存、并行度和 TempDB 配置
  • 死锁分析:及时分析和解决死锁问题
  • 性能基准:建立锁性能基准,监控变化趋势

定期审查

  • 定期审查长事务
  • 分析锁等待和阻塞事件
  • 评估索引使用情况
  • 检查隔离级别设置
  • 审查死锁历史

版本差异

SQL Server 2008/2008 R2

  • 基本的锁管理功能
  • 支持行级锁和表级锁
  • 自动死锁检测
  • 有限的 DMV 支持
  • 不支持查询存储
  • 不支持内存优化表

SQL Server 2012

  • 增强的 DMV 支持
  • 系统健康会话自动捕获死锁报告
  • 引入列存储索引,减少锁竞争
  • 支持 Always On 可用性组,影响锁行为

SQL Server 2014

  • 引入内存优化表和原生编译存储过程,使用乐观并发控制
  • 增强的索引和查询优化
  • 支持增量统计信息更新

SQL Server 2016

  • 引入查询存储,便于分析和修复执行计划
  • 增强的行版本控制
  • 支持实时查询统计
  • 引入内存优化表的并行查询支持

SQL Server 2017

  • 增强的智能查询处理功能
  • 自适应查询处理,减少锁竞争
  • 支持 Linux 平台
  • 增强的扩展事件功能

SQL Server 2019

  • 引入内存优化表的批处理支持
  • 增强的智能查询处理
  • 支持 UTF-8 字符集
  • 引入数据虚拟化功能

SQL Server 2022

  • 增强的智能查询处理
  • 支持 Query Store Hints
  • 增强的死锁检测和诊断
  • 支持 Azure Synapse Link

FAQ

如何查看当前数据库中的锁?

查看当前数据库锁的方法:

sql
-- 查看当前锁信息
SELECT 
    DB_NAME(resource_database_id) AS DatabaseName,
    OBJECT_NAME(resource_associated_entity_id) AS ObjectName,
    request_session_id AS SessionID,
    request_mode AS LockMode,
    request_status AS LockStatus,
    resource_type AS ResourceType,
    resource_description AS ResourceDescription
FROM sys.dm_tran_locks
WHERE resource_database_id = DB_ID();

如何识别锁等待和阻塞?

识别锁等待和阻塞的方法:

  • 使用 sys.dm_exec_requests 查看阻塞关系
  • 使用 sys.dm_os_waiting_tasks 查看等待任务
  • 使用 sys.dm_os_wait_stats 分析锁等待统计
  • 使用扩展事件捕获阻塞事件
  • 使用 SQL Server Management Studio 的活动监视器

如何优化锁性能?

优化锁性能的综合策略:

  • 缩短事务持续时间
  • 使用合适的隔离级别
  • 创建适当的索引
  • 优化查询语句
  • 减少锁的持有范围
  • 避免长事务
  • 监控和分析锁等待、阻塞和死锁

如何避免死锁?

避免死锁的最佳实践:

  • 保持事务简短
  • 使用相同的资源访问顺序
  • 避免在事务中执行用户交互
  • 使用较低的隔离级别
  • 添加适当的索引
  • 考虑使用乐观并发控制

锁升级会带来什么问题?如何避免?

锁升级的问题:

  • 从行级锁升级为表级锁,降低并发度
  • 导致大量事务阻塞
  • 增加死锁风险

避免锁升级的方法:

  • 创建适当的索引,减少锁竞争
  • 使用较低的隔离级别
  • 拆分大事务为小事务
  • 使用 ROWLOCK 提示(谨慎使用)
  • 启用 READ_COMMITTED_SNAPSHOT 隔离级别

READ_COMMITTED 和 READ_COMMITTED_SNAPSHOT 有什么区别?

主要区别:

  • READ_COMMITTED:使用共享锁读取数据,可能导致读-写阻塞
  • READ_COMMITTED_SNAPSHOT:使用行版本控制读取数据,消除读-写阻塞
  • 性能影响:READ_COMMITTED_SNAPSHOT 会增加一些存储开销,但提高并发度
  • 实现方式:READ_COMMITTED 依赖锁,READ_COMMITTED_SNAPSHOT 依赖 tempdb 中的行版本

如何选择合适的隔离级别?

选择隔离级别的原则:

  • 优先考虑 READ_COMMITTED 或 READ_COMMITTED_SNAPSHOT
  • 对于需要重复读取的场景,考虑 REPEATABLE READ
  • 对于需要最高一致性的场景,考虑 SERIALIZABLE
  • 对于高并发场景,考虑 SNAPSHOT 隔离级别
  • 评估业务需求和性能影响,选择平衡方案

如何监控和分析死锁?

监控和分析死锁的方法:

  • 查看系统健康会话中的死锁报告
  • 使用扩展事件捕获死锁事件
  • 分析死锁图,识别根本原因
  • 优化导致死锁的查询和事务
  • 考虑使用应用程序级别的重试机制

索引设计如何影响锁性能?

索引设计对锁性能的影响:

  • 适当的索引减少锁持有范围和时间
  • 覆盖索引避免键查找,减少锁竞争
  • 缺乏索引导致全表扫描和大范围锁定
  • 索引碎片增加锁持有时间
  • 过时的统计信息导致低效执行计划和过度锁定

如何优化长事务?

优化长事务的方法:

  • 将大事务拆分为多个小事务
  • 避免在事务中执行非数据库操作
  • 优化事务中的查询,减少执行时间
  • 使用异步处理模式
  • 考虑使用批量处理
  • 监控和识别长事务

如何使用扩展事件监控锁性能?

使用扩展事件监控锁性能的步骤:

  1. 创建扩展事件会话,添加锁相关事件
  2. 配置事件会话的目标(如事件文件)
  3. 启动事件会话
  4. 分析捕获的事件数据
  5. 优化导致锁性能问题的查询和事务

如何处理频繁的锁升级?

处理频繁锁升级的方法:

  • 识别导致锁升级的查询和事务
  • 添加适当的索引,减少锁竞争
  • 拆分大事务为小事务
  • 使用较低的隔离级别
  • 考虑使用 ROWLOCK 提示(谨慎使用)
  • 启用 READ_COMMITTED_SNAPSHOT 隔离级别

锁优化和查询优化有什么关系?

锁优化和查询优化密切相关:

  • 高效的查询减少锁持有时间和范围
  • 优化的执行计划减少锁竞争
  • 适当的索引降低锁粒度
  • 短事务减少锁等待和阻塞
  • 锁优化是查询优化的重要组成部分,反之亦然