Skip to content

SQLServer 并发控制

并发控制概述

并发控制定义

SQL Server 并发控制是指管理多个用户同时访问和修改数据库数据的机制,确保数据一致性和完整性,同时最大化系统吞吐量和响应速度。并发控制通过锁机制、隔离级别和其他技术实现,平衡数据一致性和系统性能。

并发控制的作用

生产环境作用

  • 确保数据一致性,防止并发操作导致数据损坏
  • 最大化系统吞吐量,支持更多并发用户
  • 减少锁等待和阻塞,提高系统响应速度
  • 防止死锁,确保系统稳定性
  • 支持不同隔离级别,满足不同业务需求

并发控制机制

SQL Server 提供多种并发控制机制:

  • 锁机制:悲观并发控制的基础,通过锁定资源防止并发冲突
  • 行版本控制:乐观并发控制的基础,使用版本号管理数据修改
  • 隔离级别:定义事务之间的可见性规则
  • 死锁检测与处理:自动检测和解决死锁

并发问题

脏读 (Dirty Read)

脏读是指一个事务读取了另一个未提交事务修改的数据。如果未提交事务回滚,读取的数据将是无效的。

示例

sql
-- 事务 1:修改但未提交
BEGIN TRANSACTION;
UPDATE dbo.t_Order_Header SET TotalAmount = 1000 WHERE OrderID = 1;
-- 未提交

-- 事务 2:读取未提交的数据
SELECT TotalAmount FROM dbo.t_Order_Header WHERE OrderID = 1;
-- 返回 1000(脏读)

-- 事务 1:回滚
ROLLBACK TRANSACTION;

-- 事务 2:再次读取
SELECT TotalAmount FROM dbo.t_Order_Header WHERE OrderID = 1;
-- 返回原始值,之前的读取为脏读

不可重复读 (Non-repeatable Read)

不可重复读是指一个事务对同一数据进行多次读取,但得到不同结果,因为其他事务在两次读取之间修改了数据。

示例

sql
-- 事务 1:第一次读取
BEGIN TRANSACTION;
SELECT TotalAmount FROM dbo.t_Order_Header WHERE OrderID = 1;
-- 返回 500

-- 事务 2:修改并提交
UPDATE dbo.t_Order_Header SET TotalAmount = 1000 WHERE OrderID = 1;
COMMIT TRANSACTION;

-- 事务 1:第二次读取
SELECT TotalAmount FROM dbo.t_Order_Header WHERE OrderID = 1;
-- 返回 1000(不可重复读)
COMMIT TRANSACTION;

幻读 (Phantom Read)

幻读是指一个事务执行查询后,另一个事务插入或删除了符合查询条件的行,导致第一个事务再次执行相同查询时得到不同的行数。

示例

sql
-- 事务 1:第一次查询
BEGIN TRANSACTION;
SELECT COUNT(*) FROM dbo.t_Order_Header WHERE OrderDate >= '2023-01-01';
-- 返回 10

-- 事务 2:插入新记录并提交
INSERT INTO dbo.t_Order_Header (OrderDate, TotalAmount) VALUES ('2023-01-02', 500);
COMMIT TRANSACTION;

-- 事务 1:第二次查询
SELECT COUNT(*) FROM dbo.t_Order_Header WHERE OrderDate >= '2023-01-01';
-- 返回 11(幻读)
COMMIT TRANSACTION;

丢失更新 (Lost Update)

丢失更新是指两个事务同时修改同一行数据,后提交的事务覆盖了先提交事务的修改,导致先提交的修改丢失。

示例

sql
-- 事务 1:读取并修改
BEGIN TRANSACTION;
DECLARE @Amount DECIMAL(18,2);
SELECT @Amount = TotalAmount FROM dbo.t_Order_Header WHERE OrderID = 1;
-- @Amount = 500
WAITFOR DELAY '00:00:05'; -- 模拟延迟
UPDATE dbo.t_Order_Header SET TotalAmount = @Amount + 100 WHERE OrderID = 1;
-- 设置为 600
COMMIT TRANSACTION;

-- 事务 2:同时读取并修改
BEGIN TRANSACTION;
DECLARE @Amount DECIMAL(18,2);
SELECT @Amount = TotalAmount FROM dbo.t_Order_Header WHERE OrderID = 1;
-- @Amount = 500(与事务 1 同时读取)
UPDATE dbo.t_Order_Header SET TotalAmount = @Amount + 200 WHERE OrderID = 1;
-- 设置为 700,覆盖了事务 1 的修改
COMMIT TRANSACTION;

-- 最终结果
SELECT TotalAmount FROM dbo.t_Order_Header WHERE OrderID = 1;
-- 返回 700,事务 1 的修改丢失

锁机制

锁的类型

生产环境常见锁类型

  • 共享锁 (S):用于读取操作,允许多个事务同时读取同一资源
  • 排他锁 (X):用于修改操作,防止其他事务读取或修改同一资源
  • 更新锁 (U):用于更新操作的初始阶段,防止死锁
  • 意向锁 (IS, IX, SIX):表示事务对资源的部分或全部持有锁的意向
  • 架构锁 (Sch-S, Sch-M):用于保护数据库架构变更

锁的粒度

锁的粒度指锁保护的资源大小,SQL Server 支持多级粒度:

  • 行级锁:保护单行数据,并发度最高
  • 页级锁:保护 8KB 数据页
  • 表级锁:保护整个表
  • 数据库级锁:保护整个数据库

锁的兼容性

锁的兼容性决定了一个事务持有某种锁时,其他事务能否同时持有相同资源的其他锁。例如:

  • 共享锁与共享锁兼容
  • 共享锁与排他锁不兼容
  • 排他锁与任何锁都不兼容

锁的升级

锁升级是指 SQL Server 将多个细粒度锁(如行级锁)升级为粗粒度锁(如表级锁),以减少锁管理开销。锁升级可能导致并发度下降,需要合理设计索引和查询来避免。

乐观并发控制

乐观并发控制概述

乐观并发控制假设并发冲突很少发生,允许事务自由执行,只在提交时检查是否发生冲突。如果发生冲突,事务将回滚并重新执行。

实现乐观并发控制

生产环境实现方式

  • 时间戳列:添加时间戳列,修改时检查时间戳是否变化
  • 版本号列:添加版本号列,修改时递增版本号并检查
  • 比较所有列:修改时比较所有列的值是否变化

示例

sql
-- 添加版本号列
ALTER TABLE dbo.t_Order_Header ADD Version INT DEFAULT 0;

-- 乐观并发更新
DECLARE @OrderID INT = 1;
DECLARE @NewTotalAmount DECIMAL(18,2) = 1000;
DECLARE @CurrentVersion INT;

-- 获取当前数据和版本号
SELECT @CurrentVersion = Version FROM dbo.t_Order_Header WHERE OrderID = @OrderID;

-- 更新时检查版本号
UPDATE dbo.t_Order_Header
SET TotalAmount = @NewTotalAmount, Version = Version + 1
WHERE OrderID = @OrderID AND Version = @CurrentVersion;

-- 检查是否更新成功
IF @@ROWCOUNT = 0
BEGIN
    -- 并发冲突,处理逻辑
    PRINT '并发冲突,更新失败';
END
ELSE
BEGIN
    PRINT '更新成功';
END

乐观并发控制的优缺点

优点

  • 减少锁竞争,提高并发度
  • 适合读多写少的场景
  • 减少死锁风险

缺点

  • 冲突时需要回滚和重试,增加应用复杂度
  • 不适合写多读少的场景
  • 可能导致活锁(事务反复重试)

最佳实践

  • 适合读多写少的系统
  • 实现合理的重试机制,避免活锁
  • 使用时间戳或版本号列,而不是比较所有列
  • 在应用层实现冲突处理逻辑

悲观并发控制

悲观并发控制概述

悲观并发控制假设并发冲突经常发生,通过锁机制防止并发冲突。事务在操作数据前先锁定资源,直到事务完成才释放锁。

实现悲观并发控制

生产环境实现方式

  • 使用默认锁:依赖 SQL Server 默认的锁机制
  • 使用锁提示:显式指定锁类型和粒度
  • 使用 sp_getapplock:实现应用程序级别的锁

示例

sql
-- 使用 UPDLOCK 提示防止丢失更新
BEGIN TRANSACTION;

DECLARE @Amount DECIMAL(18,2);
SELECT @Amount = TotalAmount FROM dbo.t_Order_Header WITH (UPDLOCK) WHERE OrderID = 1;
-- 获取更新锁,防止其他事务修改

-- 模拟处理延迟
WAITFOR DELAY '00:00:05';

-- 更新数据
UPDATE dbo.t_Order_Header SET TotalAmount = @Amount + 100 WHERE OrderID = 1;

COMMIT TRANSACTION;
-- 释放锁

悲观并发控制的优缺点

优点

  • 实现简单,不需要应用层处理冲突
  • 适合写多读少的场景
  • 确保事务的原子性和一致性

缺点

  • 锁竞争严重,并发度低
  • 可能导致死锁和阻塞
  • 锁持有时间长,影响系统响应速度

最佳实践

  • 适合写多读少的系统
  • 保持事务简短,减少锁持有时间
  • 合理使用锁提示,避免过度锁定
  • 监控锁等待和阻塞

快照隔离

快照隔离概述

快照隔离是 SQL Server 2005 引入的基于行版本控制的隔离级别,允许事务读取数据的快照,而不是锁定数据。快照隔离可以消除读-写阻塞,提高并发度。

实现快照隔离

生产环境配置

sql
-- 启用 READ_COMMITTED_SNAPSHOT(推荐)
ALTER DATABASE [ECommerce] SET READ_COMMITTED_SNAPSHOT ON;

-- 或启用 SNAPSHOT 隔离级别
ALTER DATABASE [ECommerce] SET ALLOW_SNAPSHOT_ISOLATION ON;

使用快照隔离

sql
-- 使用 SNAPSHOT 隔离级别
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

BEGIN TRANSACTION;

-- 读取快照数据,不会阻塞其他事务修改
SELECT * FROM dbo.t_Order_Header;

-- 事务提交
COMMIT TRANSACTION;

快照隔离的优缺点

优点

  • 消除读-写阻塞,提高并发度
  • 支持复杂查询而不锁定数据
  • 适合混合工作负载

缺点

  • 增加 tempdb 存储空间和 I/O 开销
  • 可能导致更新冲突,需要应用层处理
  • 不适合需要绝对最新数据的场景

最佳实践

  • 优先使用 READ_COMMITTED_SNAPSHOT,配置简单且效果好
  • 确保 tempdb 有足够空间和性能
  • 监控 tempdb 使用情况
  • 适合混合工作负载和高并发场景

并发控制策略

选择合适的并发控制机制

生产环境选择原则

  • 读多写少:乐观并发控制或快照隔离
  • 写多读少:悲观并发控制
  • 混合工作负载:快照隔离或结合使用
  • 简单应用:悲观并发控制
  • 复杂应用:乐观并发控制

合理使用隔离级别

生产环境隔离级别选择

  • READ COMMITTED:默认级别,适合大多数场景
  • READ COMMITTED SNAPSHOT:消除读-写阻塞,推荐使用
  • REPEATABLE READ:防止不可重复读,并发度较低
  • SNAPSHOT:完全消除读-写阻塞,需要额外配置
  • SERIALIZABLE:最高隔离级别,并发度最低,仅在必要时使用

优化查询语句

生产环境查询优化

  • 使用索引覆盖查询,减少锁范围
  • 避免全表扫描,减少锁竞争
  • 优化 JOIN 操作,减少锁持有时间
  • 使用 TOP 或 OFFSET-FETCH 限制结果集

减少锁的持有时间

生产环境实践

  • 保持事务简短,尽快提交或回滚
  • 避免在事务中执行非数据库操作
  • 优化事务中的查询,减少执行时间
  • 将大事务拆分为小事务

并发性能优化

索引优化

生产环境索引优化

  • 创建合适的索引,减少锁竞争
  • 使用覆盖索引,避免键查找
  • 定期维护索引,减少碎片
  • 更新统计信息,确保查询优化器生成高效执行计划

查询优化

生产环境查询优化

  • 优化查询计划,减少锁持有时间
  • 避免使用游标,使用集合并发操作
  • 减少排序和聚合操作
  • 合理使用查询提示,避免过度锁定

事务优化

生产环境事务优化

  • 使用短事务,减少锁持有时间
  • 避免长事务,防止锁积累
  • 使用合适的隔离级别
  • 合理组织事务中的操作顺序

系统配置优化

生产环境系统配置

  • 优化内存配置,减少 I/O 等待
  • 配置合适的并行度,避免过度并行
  • 优化 TempDB 配置,提高行版本控制性能
  • 配置最大并发连接数,平衡系统负载

并发监控与调试

监控锁等待

生产环境监控方法

sql
-- 查看锁等待统计
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 'LCK_%'
ORDER BY wait_time_ms DESC;

监控阻塞

生产环境监控方法

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;

监控死锁

生产环境监控方法

sql
-- 查看死锁信息
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);

分析执行计划

分析查询执行计划,识别锁问题:

  • 查看执行计划中的锁提示和警告
  • 识别全表扫描和大范围锁定
  • 优化执行计划,减少锁竞争

并发控制最佳实践

最小化锁的粒度

  • 使用行级锁,避免表级锁
  • 创建合适的索引,减少锁范围
  • 避免使用表级锁提示

最小化锁的持有时间

  • 保持事务简短
  • 避免在事务中执行非数据库操作
  • 优化查询,减少执行时间

使用合适的隔离级别

  • 优先使用 READ_COMMITTED_SNAPSHOT
  • 避免使用 SERIALIZABLE 隔离级别
  • 根据业务需求选择合适的隔离级别

优化事务设计

  • 使用短事务,避免长事务
  • 合理组织事务中的操作顺序
  • 考虑使用异步处理模式

版本差异

SQL Server 2008/2008 R2

  • 基本的锁机制和隔离级别
  • 支持快照隔离,但需要手动配置
  • 有限的 DMV 支持用于监控
  • 不支持 READ_COMMITTED_SNAPSHOT 自动配置

SQL Server 2012

  • 增强的 DMV 支持
  • 系统健康会话自动捕获死锁报告
  • 支持 READ_COMMITTED_SNAPSHOT
  • 引入列存储索引,减少锁竞争

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

乐观并发控制和悲观并发控制有什么区别?

主要区别:

  • 假设:乐观并发假设冲突少,悲观并发假设冲突多
  • 实现方式:乐观使用版本控制,悲观使用锁机制
  • 冲突处理:乐观在提交时检测冲突,悲观在操作前防止冲突
  • 并发度:乐观并发度高,悲观并发度低
  • 适用场景:乐观适合读多写少,悲观适合写多读少

如何选择合适的并发控制机制?

选择原则:

  • 考虑数据访问模式(读多写少或写多读少)
  • 评估系统负载和并发用户数
  • 考虑应用程序复杂度
  • 权衡开发成本和运行时性能
  • 测试不同机制的性能表现

如何避免并发问题?

避免并发问题的方法:

  • 使用合适的隔离级别
  • 选择合适的并发控制机制
  • 优化查询和事务
  • 减少锁持有时间
  • 监控和分析并发问题
  • 实现合理的冲突处理逻辑

如何优化并发性能?

优化并发性能的策略:

  • 使用快照隔离或乐观并发控制
  • 优化索引和查询
  • 保持事务简短
  • 减少锁持有时间和粒度
  • 优化系统配置
  • 监控和调优锁等待和阻塞

如何监控并发问题?

监控并发问题的方法:

  • 使用 DMVs 查看锁等待和阻塞
  • 使用扩展事件捕获并发事件
  • 分析死锁报告
  • 监控系统性能计数器
  • 使用第三方监控工具

快照隔离和 READ_COMMITTED_SNAPSHOT 有什么区别?

主要区别:

  • 配置方式:READ_COMMITTED_SNAPSHOT 是数据库级别设置,快照隔离需要事务显式指定
  • 事务隔离:READ_COMMITTED_SNAPSHOT 替换默认的 READ_COMMITTED 行为,快照隔离是独立的隔离级别
  • 更新冲突:快照隔离会检测更新冲突,READ_COMMITTED_SNAPSHOT 不会
  • tempdb 使用:两者都使用 tempdb 存储行版本

如何处理死锁?

处理死锁的方法:

  • 分析死锁报告,识别根本原因
  • 优化导致死锁的查询和事务
  • 使用相同的资源访问顺序
  • 保持事务简短
  • 减少锁持有时间
  • 考虑使用乐观并发控制
  • 实现应用程序级别的重试机制

锁升级会导致什么问题?如何避免?

锁升级的问题:

  • 降低并发度,导致大量事务阻塞
  • 增加死锁风险
  • 影响系统响应速度

避免锁升级的方法:

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

如何设计高并发系统?

高并发系统设计原则:

  • 选择合适的并发控制机制
  • 优化数据库设计和索引
  • 实现缓存层,减少数据库访问
  • 采用微服务架构,分散负载
  • 优化系统配置
  • 监控和调优系统性能

并发控制对性能有什么影响?

并发控制对性能的影响:

  • 锁机制会增加系统开销,降低并发度
  • 行版本控制会增加 tempdb 负载
  • 隔离级别越高,性能开销越大
  • 合理的并发控制可以提高系统吞吐量
  • 不当的并发控制会导致大量锁等待和阻塞