事务、锁与并发控制

事务、锁与并发控制

事务、锁与并发控制

5.1 事务基础与 ACID 属性

5.1.1 ACID 特性详解

属性 英文 SQL Server 实现机制 说明
原子性 Atomicity 事务日志 + ROLLBACK 事务中的所有操作要么全部成功,要么全部回滚
一致性 Consistency 约束 + 触发器 + 应用逻辑 事务将数据库从一个合法状态转为另一个合法状态
隔离性 Isolation 锁 + 行版本控制 并发事务彼此隔离,互不干扰
持久性 Durability Write-Ahead Logging (WAL) 已提交的事务不会因崩溃丢失

5.1.2 事务类型与控制语句

-- ① 显式事务(最常用,推荐在应用层使用)
BEGIN TRANSACTION;   -- 或 BEGIN TRAN

    -- 事务内的操作
    UPDATE Accounts SET Balance = Balance - 1000 WHERE AccountID = 101;
    UPDATE Accounts SET Balance = Balance + 1000 WHERE AccountID = 202;

    -- 检查是否有错误
    IF @@ERROR <> 0
    BEGIN
        ROLLBACK TRANSACTION;
        RETURN;
    END;

COMMIT TRANSACTION;  -- 或 COMMIT TRAN / COMMIT

-- ② 推荐:TRY...CATCH 事务模式
BEGIN TRY
    BEGIN TRANSACTION;

    UPDATE Accounts SET Balance = Balance - 1000 WHERE AccountID = 101;
    UPDATE Accounts SET Balance = Balance + 1000 WHERE AccountID = 202;

    -- 人为测试回滚:RAISERROR('测试错误', 16, 1);

    COMMIT TRANSACTION;
    PRINT '转账成功';

END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;

    -- 记录错误日志
    INSERT INTO dbo.ErrorLog (ErrorMsg, ErrorLine, ErrorProc, ErrorTime)
    VALUES (ERROR_MESSAGE(), ERROR_LINE(), ERROR_PROCEDURE(), GETDATE());

    -- 重新抛出错误(SQL Server 2012+,保留原始错误信息)
    THROW;
    -- 旧写法:RAISERROR(ERROR_MESSAGE(), ERROR_SEVERITY(), ERROR_STATE());
END CATCH;

-- ③ 自动提交(默认模式,每条语句自动构成一个事务)
UPDATE Accounts SET Balance = 5000 WHERE AccountID = 101;  -- 自动 COMMIT

-- ④ 隐式事务(不建议使用,容易忘记 COMMIT)
SET IMPLICIT_TRANSACTIONS ON;
-- 之后的 DML/DDL 自动 BEGIN TRAN,需手动 COMMIT/ROLLBACK
UPDATE Accounts SET Balance = 5000 WHERE AccountID = 101;
COMMIT;   -- 必须手动提交!
SET IMPLICIT_TRANSACTIONS OFF;

-- ⑤ 保存点(Savepoint):部分回滚
BEGIN TRANSACTION;
    INSERT INTO Orders (CustomerID, Amount) VALUES (1, 100);
    SAVE TRANSACTION sp1;   -- 保存点

    INSERT INTO OrderItems (OrderID, ProductID) VALUES (SCOPE_IDENTITY(), 999);
    -- 假设 ProductID=999 不存在,违反外键约束

    IF @@ERROR <> 0
        ROLLBACK TRANSACTION sp1;  -- 只回滚到保存点,保留 Orders 的插入

COMMIT TRANSACTION;  -- 提交 Orders 的插入(OrderItems 已回滚)

5.1.3 事务日志工作机制(WAL)

-- 查看事务日志使用情况
SELECT
    name,
    log_size_mb     = SIZE * 8.0 / 1024,
    log_used_mb     = FILEPROPERTY(name, 'SpaceUsed') * 8.0 / 1024,
    log_used_pct    = CAST(FILEPROPERTY(name, 'SpaceUsed') * 100.0 / SIZE AS DECIMAL(5,2)),
    recovery_model  = DATABASEPROPERTYEX(name, 'Recovery')
FROM sys.databases
WHERE database_id > 4;  -- 排除系统数据库

-- 查看活跃日志
DBCC SQLPERF(LOGSPACE);

-- 查看事务日志 VLF 状态(VLF=Virtual Log File)
DBCC LOGINFO('YourDatabase');

-- 日志增长管理:避免日志文件过度增长
-- 问题:事务日志 VLF 碎片化会导致性能下降
-- 建议:初始分配足够大;增量设置为固定值(非百分比)
ALTER DATABASE YourDatabase
MODIFY FILE (NAME = 'YourDatabase_log', SIZE = 4096MB, FILEGROWTH = 512MB);

5.2 锁机制详解

5.2.1 锁类型层次

SQL Server 的锁体系按粒度从细到粗:

行锁(Row)
  ↓  当行数超过阈值时自动升级
页锁(Page = 8KB)
  ↓  当页数超过阈值时自动升级
表锁(Table)

主要锁模式:

锁模式 符号 说明 与其他锁的兼容性
共享锁 S 读取数据时申请 与 S 兼容,与 X 不兼容
排他锁 X 修改数据时申请 与任何锁不兼容
意向共享锁 IS 表示后代对象有 S 锁 与大多数兼容
意向排他锁 IX 表示后代对象有 X 锁 与 S/X/IS 不兼容
共享意向排他锁 SIX 当前持有 S 锁并打算申请 X 严格限制
更新锁 U 防止转换死锁(先 S 后 X 的中间状态) 与 S 兼容
架构稳定锁 Sch-S 查询期间防止表结构变更
架构修改锁 Sch-M DDL 操作时申请 与几乎所有锁不兼容
-- 查看当前锁请求
SELECT
    request_session_id AS SPID,
    resource_type,
    resource_description,
    request_mode,
    request_status,
    resource_associated_entity_id
FROM sys.dm_tran_locks
WHERE request_session_id > 50  -- 排除系统会话
ORDER BY request_session_id, resource_type;

-- 查看活跃事务
SELECT
    t.transaction_id,
    t.name,
    t.transaction_begin_time,
    DATEDIFF(SECOND, t.transaction_begin_time, GETDATE()) AS DurationSec,
    t.transaction_type,
    t.transaction_state,
    s.session_id,
    s.login_name,
    s.host_name,
    r.command,
    SUBSTRING(txt.text, (r.statement_start_offset/2)+1, 200) AS CurrentSQL
FROM sys.dm_tran_active_transactions t
JOIN sys.dm_tran_session_transactions st ON t.transaction_id = st.transaction_id
JOIN sys.dm_exec_sessions s             ON st.session_id = s.session_id
LEFT JOIN sys.dm_exec_requests r        ON s.session_id = r.session_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) txt
WHERE t.transaction_begin_time IS NOT NULL
ORDER BY t.transaction_begin_time;

5.2.2 死锁分析与解决

死锁发生在两个或多个会话互相等待对方持有的锁,SQL Server 会自动选择”代价最小的”事务作为死锁牺牲品(Deadlock Victim)。

-- 模拟死锁场景:
-- 会话1:先锁 A,再锁 B
-- 会话2:先锁 B,再锁 A

-- 会话1 执行:
BEGIN TRAN;
UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = 1;
-- 等待会话2...(手动在会话2中先运行)
WAITFOR DELAY '00:00:05';
UPDATE Accounts SET Balance = Balance + 100 WHERE AccountID = 2;
COMMIT;

-- 会话2 执行(同时):
BEGIN TRAN;
UPDATE Accounts SET Balance = Balance - 200 WHERE AccountID = 2;
WAITFOR DELAY '00:00:05';
UPDATE Accounts SET Balance = Balance + 200 WHERE AccountID = 1;  -- 死锁!
COMMIT;

-- 捕获死锁信息(系统健康监测会话记录)
WITH SystemHealthDeadlocks AS (
    SELECT CAST(target_data AS XML) AS HealthData
    FROM sys.dm_xe_session_targets t
    JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address
    WHERE s.name = 'system_health' AND t.target_name = 'ring_buffer'
)
SELECT
    event_data.value('(event/@timestamp)[1]', 'DATETIME2') AS DeadlockTime,
    event_data.query('(event/data[@name="xml_report"]/value/deadlock)[1]') AS DeadlockGraph
FROM SystemHealthDeadlocks
CROSS APPLY HealthData.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(event_data)
ORDER BY DeadlockTime DESC;

-- 应用层防止死锁的最佳实践
-- 1. 统一访问顺序(所有事务按相同顺序访问表)
-- 2. 缩短事务持续时间(避免在事务中等待用户输入)
-- 3. 设置死锁优先级
SET DEADLOCK_PRIORITY LOW;   -- 让当前会话优先成为牺牲品(保护重要事务)
SET DEADLOCK_PRIORITY HIGH;  -- 让当前会话不被牺牲(用于重要批处理)

-- 4. 使用 NOLOCK/READ COMMITTED SNAPSHOT 减少读锁
-- 5. 应用层捕获错误1205并重试

5.2.3 锁升级与锁超时

-- 锁超时设置(避免无限等待)
SET LOCK_TIMEOUT 5000;   -- 等待5秒,超时后抛出错误1222
-- SET LOCK_TIMEOUT -1;  -- 无限等待(默认)
-- SET LOCK_TIMEOUT 0;   -- 立即超时(不等待)

BEGIN TRY
    SET LOCK_TIMEOUT 5000;
    UPDATE Orders SET Status = 2 WHERE OrderID = 1001;
    COMMIT;
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() = 1222  -- Lock request time out
    BEGIN
        PRINT '锁等待超时,请稍后重试';
    END
    ELSE THROW;
END CATCH;

-- 锁升级阈值(默认:当单个表的锁数量超过5000时升级为表锁)
-- 可以在表级别禁用锁升级(适合大表高并发场景)
ALTER TABLE dbo.Orders SET (LOCK_ESCALATION = DISABLE);
-- 或限制只升级到分区锁(分区表)
ALTER TABLE dbo.OrdersPartitioned SET (LOCK_ESCALATION = AUTO);

5.3 隔离级别详解

5.3.1 传统隔离级别

-- SQL Server 支持的隔离级别
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;    -- 读未提交(脏读)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;      -- 读已提交(默认)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;     -- 可重复读
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;        -- 串行化

-- 查看当前会话隔离级别
SELECT
    CASE transaction_isolation_level
        WHEN 0 THEN 'Unspecified'
        WHEN 1 THEN 'READ UNCOMMITTED'
        WHEN 2 THEN 'READ COMMITTED'
        WHEN 3 THEN 'REPEATABLE READ'
        WHEN 4 THEN 'SERIALIZABLE'
        WHEN 5 THEN 'SNAPSHOT'
    END AS IsolationLevel
FROM sys.dm_exec_sessions
WHERE session_id = @@SPID;

各隔离级别的并发问题:

隔离级别 脏读 不可重复读 幻读 并发性
READ UNCOMMITTED ❌允许 ❌允许 ❌允许 最高
READ COMMITTED ✅防止 ❌允许 ❌允许
REPEATABLE READ ✅防止 ✅防止 ❌允许
SERIALIZABLE ✅防止 ✅防止 ✅防止 最低
SNAPSHOT ✅防止 ✅防止 ✅防止 高(行版本)
READ COMMITTED SNAPSHOT ✅防止 ❌允许 ❌允许 高(行版本)
-- 演示:脏读(READ UNCOMMITTED)
-- 会话1
BEGIN TRAN;
UPDATE Products SET Price = 9999 WHERE ProductID = 1;
-- 不提交,等待...

-- 会话2(可以读取到未提交的 9999)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT Price FROM Products WHERE ProductID = 1;  -- 读到 9999(脏读!)

-- NOLOCK 提示等效于 READ UNCOMMITTED
SELECT Price FROM Products WITH (NOLOCK) WHERE ProductID = 1;
-- ⚠️ 不推荐在生产环境对业务表使用 NOLOCK,可能读到不一致数据

5.3.2 行版本控制隔离(RCSI 与 Snapshot)

行版本控制不依赖锁来实现读隔离,而是读取行的历史版本(存储在 TempDB 的版本存储区),读不阻塞写,写不阻塞读,可显著提升并发。

-- 启用 READ_COMMITTED_SNAPSHOT(RCSI)
-- 将默认的 READ COMMITTED 级别由锁方式改为行版本方式
-- 生产建议:维护窗口执行,此操作需要数据库独占访问
ALTER DATABASE YourDatabase SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
ALTER DATABASE YourDatabase SET READ_COMMITTED_SNAPSHOT ON;
ALTER DATABASE YourDatabase SET MULTI_USER;

-- 验证 RCSI 是否开启
SELECT name,
       is_read_committed_snapshot_on,
       snapshot_isolation_state_desc
FROM sys.databases
WHERE name = DB_NAME();

-- 启用快照隔离(Snapshot Isolation,SI)
-- SI 与 RCSI 的区别:
-- RCSI:语句级别快照(每条语句看到自己开始时的快照)
-- SI:事务级别快照(整个事务看到事务开始时的快照)
ALTER DATABASE YourDatabase SET ALLOW_SNAPSHOT_ISOLATION ON;

-- 使用快照隔离读取
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRAN;
SELECT * FROM Products;  -- 读取事务开始时的数据版本
-- 期间其他事务的修改对本事务不可见
COMMIT;

-- 版本存储监控(版本存储在 TempDB)
SELECT
    version_store_reserved_page_count * 8.0 / 1024 AS VersionStoreMB,
    version_store_start_time,
    version_cleanup_estimate_ms
FROM sys.dm_tran_version_store_space_usage;

-- 查找长时间运行的快照事务(影响版本存储增长)
SELECT
    s.session_id,
    s.login_name,
    t.transaction_begin_time,
    DATEDIFF(MINUTE, t.transaction_begin_time, GETDATE()) AS DurationMin,
    s.host_name,
    r.command
FROM sys.dm_tran_active_snapshot_database_transactions t
JOIN sys.dm_exec_sessions s ON t.session_id = s.session_id
LEFT JOIN sys.dm_exec_requests r ON s.session_id = r.session_id
ORDER BY t.transaction_begin_time;

5.4 阻塞分析与实战

-- 完整的阻塞分析查询(生产环境常用)
WITH BlockingChain AS (
    SELECT
        r.session_id                AS BlockedSPID,
        r.blocking_session_id       AS BlockerSPID,
        r.wait_type,
        r.wait_time / 1000          AS WaitSec,
        r.status,
        s.login_name                AS BlockedLogin,
        s.host_name                 AS BlockedHost,
        bs.login_name               AS BlockerLogin,
        bs.host_name                AS BlockerHost,
        SUBSTRING(t.text,
            (r.statement_start_offset/2)+1,
            ((CASE r.statement_end_offset
              WHEN -1 THEN DATALENGTH(t.text)
              ELSE r.statement_end_offset END - r.statement_start_offset)/2)+1
        )                           AS BlockedSQL,
        bt.text                     AS BlockerSQL
    FROM sys.dm_exec_requests r
    JOIN sys.dm_exec_sessions s  ON r.session_id = s.session_id
    JOIN sys.dm_exec_sessions bs ON r.blocking_session_id = bs.session_id
    CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
    OUTER APPLY (
        SELECT TOP 1 st.text
        FROM sys.dm_exec_connections c
        CROSS APPLY sys.dm_exec_sql_text(c.most_recent_sql_handle) st
        WHERE c.session_id = r.blocking_session_id
    ) bt(text)
    WHERE r.blocking_session_id > 0
)
SELECT * FROM BlockingChain ORDER BY WaitSec DESC;

-- 强制终止阻塞会话(谨慎使用!)
-- KILL {session_id};
-- KILL {session_id} WITH STATUSONLY;  -- 查看回滚进度

5.5 小结

知识点 关键结论
事务控制 始终用 TRY…CATCH;XACT_ABORT ON 可自动回滚
死锁预防 统一访问顺序;缩短事务时长;使用行版本隔离
RCSI 生产推荐开启,彻底消灭读写互阻
NOLOCK 开发中非常危险,可能读到不一致数据,避免使用
锁升级 大表高并发场景可禁用锁升级
隔离级别 READ COMMITTED(锁)→ RCSI(行版本)是最常见的升级路径

下一章:存储过程、函数与动态 SQL

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有
  • 上一篇:
  • 下一篇: