第八期:存储引擎深度(四)—— 事务与并发控制的内部实现

0 阅读8分钟

### 第八期:存储引擎深度(四)—— 事务与并发控制的内部实现

#### 1. 事务的ACID与SQL Server的实现层次

| ACID属性 | SQL Server实现机制 | 关键组件 |

|----------|-------------------|----------|

| **原子性(Atomicity)** | 事务日志 + 撤消(Undo) | 事务日志,LSN |

| **一致性(Consistency)** | 约束、触发器、隔离级别 | 查询处理器,锁管理器 |

| **隔离性(Isolation)** | 锁 + 行版本控制 | 锁管理器,tempdb |

| **持久性(Durability)** | WAL + 检查点 + 恢复 | 事务日志,检查点 |

**核心原则回顾**:WAL(Write-Ahead Logging)—— 日志写入必须在数据页写入之前完成。

#### 2. 深入事务日志:LSN与日志链

**LSN(Log Sequence Number)**:每个日志记录的唯一标识,由三部分构成:

- **VLF序号**(2字节):虚拟日志文件编号

- **块偏移量**(4字节):日志块在VLF中的位置

- **日志槽号**(2字节):块内的日志记录序号

**日志记录的关键字段**:


+--------------+----------+----------+-----------+------+

| 事务ID | LSN前向指针 | 操作类型 | 页ID(前/后镜像)| ... |

+--------------+----------+----------+-----------+------+

- **前向指针**:形成日志链,支持正向扫描(恢复时重做)和反向扫描(回滚时撤消)

- **操作类型**:LOP_BEGIN_XACT, LOP_COMMIT_XACT, LOP_INSERT_ROWS, LOP_MODIFY_ROW, LOP_DELETE_ROWS 等

**日志截断与日志备份的关系**:


简单恢复模式:检查点 → 截断非活动VLF

完整恢复模式:日志备份 → 标记VLF为可重用(但必须日志备份后)

大容量日志恢复模式:最小化日志操作(SELECT INTO, 索引重建)记录更少

**现象解释**:

- 为什么完整恢复模式下日志一直增长?因为没有做日志备份(log\_reuse\_wait\_desc = LOG\_BACKUP

- 为什么大事务回滚非常慢?回滚需要重新扫描日志链,找到所有撤消记录并重放

#### 3. 行版本控制(Row Versioning)内部机制

**行版本的工作原理**:

1. 修改行时,不直接覆盖原行,而是在tempdb中存储行的先前版本

2. 每个行版本带有一个**事务序列号(XSN)**

3. 读操作根据隔离级别,选择合适版本的行

**两种行版本隔离级别**:

| 隔离级别 | 读操作行为 | 写操作行为 | 配置 |

|----------|-----------|-----------|------|

| **READ COMMITTED SNAPSHOT(RCSI)** | 读取语句开始时已提交的最新行版本 | 仍然使用锁(X锁,U锁) | 数据库级设置 |

| **SNAPSHOT ISOLATION** | 读取事务开始时已提交的行版本 | 检测写冲突(更新冲突会失败) | 数据库级+事务级设置 |

**行版本的结构(存储在tempdb中)**:


+---------------------------+----------+----------+------+

| 版本头(XSN、长度、指针) | 前镜像数据 | 后镜像数据 | 链指针 |

+---------------------------+----------+----------+------+

- 版本链:通过指向tempdb中前一个版本的指针形成

- 版本清理:当没有任何活动事务需要该版本时,定期清理

**启用行版本控制**:


\-- 检查是否启用

SELECT name, is\_read\_committed\_snapshot\_on, snapshot\_isolation\_state

FROM sys.databases WHERE name = 'YourDB';



\-- 启用RCSI(需要数据库独占,重启现有连接)

ALTER DATABASE YourDB SET READ\_COMMITTED\_SNAPSHOT ON;



\-- 启用快照隔离

ALTER DATABASE YourDB SET ALLOW\_SNAPSHOT\_ISOLATION ON;



\-- 事务级别使用快照隔离

SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

BEGIN TRAN

\-- 查询看不到事务开始后的任何修改

COMMIT

**现象解释**:

- 为什么开启RCSI后,读写不再互相阻塞?因为读使用tempdb中的行版本,不需要共享锁

- 为什么tempdb空间可能会暴增?长事务导致大量行版本保留,无法清理

#### 4. 恢复过程详解(ARIES恢复模型)

SQL Server使用ARIES(Algorithms for Recovery and Isolation Exploiting Semantics)理论的变体。

**恢复的三大阶段**:

**阶段1:分析(Analysis)**

- 从上一个检查点开始扫描日志

- 找出检查点时所有活动事务

- 确定重做(Redo)的起始LSN(最老脏页对应的LSN)

**阶段2:重做(Redo)**

- 从分析阶段确定的起始LSN开始,正向扫描日志

- 对所有已提交事务的修改,重新应用到数据页(确保磁盘上的页与日志一致)

- 幂等操作:重复应用不会破坏一致性

**阶段3:撤消(Undo)**

- 反向扫描日志,回滚所有未提交事务

- 生成撤消日志(也会写入日志,确保崩溃恢复时能再次撤消)

- 最后标记事务为已回滚

**示例恢复场景**:


检查点LSN=100,当前LSN=500

日志内容:

100-200: 事务T1的各种操作

200-300: 事务T2操作,T2在LSN=300提交

300-400: T1继续操作

400-500: 事务T3操作,未提交



崩溃后恢复:

1\. 分析:活动事务为T1(未提交)、T3(未提交),Redo起始LSN=100

2\. 重做:重新应用LSN=100500的所有已提交事务(T2)

3\. 撤消:反向回滚T1和T3的修改

**检查点类型**:

| 检查点类型 | 触发条件 | 写入内容 |

|------------|----------|----------|

| 自动检查点 | 按恢复间隔(默认1分钟)或日志增长 | 所有脏页列表 |

| 手动检查点 | CHECKPOINT命令 | 所有脏页 |

| 间接检查点 | TARGET_RECOVERY_TIME设置 | 平滑写入,避免尖峰 |

| 内部检查点 | 备份、DBCC、关机 | 确保数据文件一致 |

#### 5. 事务的三种回滚场景

**场景一:显式回滚**


BEGIN TRAN

UPDATE Orders SET Status='Cancelled' WHERE OrderID=101

ROLLBACK  -- 从日志中提取撤消信息,直接回滚

**场景二:自动回滚(运行时错误)**

- 严重级别11-16:通常会回滚当前语句,但事务保持活动

- 严重级别17+:可能回滚整个事务

**场景三:死锁牺牲品**

- 检测到死锁,选择牺牲品 → 回滚整个事务(错误1205)

**回滚内部机制**:

1. 从日志中定位事务的开始记录(LOP_BEGIN_XACT)

2. 反向扫描日志链(通过前向指针或从末尾反向)

3. 为每个操作生成撤消记录(可能写入日志)

4. 释放该事务持有的所有锁

5. 标记事务为已回滚

**现象解释**:

- 为什么回滚很慢但回滚过程中看不到进度?因为回滚在后台完全执行,直到完成才释放锁

- 为什么回滚时日志还在增长?因为撤消操作本身会生成日志

#### 6. 监控事务与恢复的关键DMV

| DMV/命令 | 信息 | 关键列 |

|----------|------|--------|

| sys.dm\_tran\_active\_transactions | 活动事务列表 | transaction_begin_time, transaction_type, transaction_state |

| sys.dm\_tran\_database\_transactions | 数据库级事务信息 | database_transaction_log_bytes_used, database_transaction_log_record_count |

| sys.dm\_tran\_session\_transactions | 会话与事务关联 | session_id, transaction_id |

| sys.dm\_tran\_current\_snapshot | 当前快照事务 | snapshot_sequence_number |

| DBCC OPENTRAN | 最早活动事务 | 主机名、程序名、开始时间 |

| sys.dm\_exec\_requests | 当前请求的等待状态 | wait_type, wait_time, blocking_session_id |

**监控行版本使用**:


\-- 查看tempdb行版本使用情况

SELECT version\_store\_reserved\_page\_count, version\_store\_reserved\_kb 

FROM sys.dm\_db\_file\_space\_usage;



\-- 查看生成行版本最多的事务

SELECT TOP 10 transaction\_id, version\_chain\_traversed, page\_count

FROM sys.dm\_tran\_version\_store

ORDER BY page\_count DESC;

#### 7. 常见问题与优化

| 问题 | 根本原因 | 排查方法 | 解决方案 |

|------|----------|----------|----------|

| 恢复时间过长 | 检查点间隔过大,或间接检查点目标时间长 | 查看恢复时间设置 | 调整TARGET\_RECOVERY\_TIME |

| tempdb暴增 | 长事务导致行版本积压 | version\_store\_reserved\_kb持续增长 | 优化长事务,定期提交 |

| 更新冲突(快照隔离) | 两个事务同时更新同一行 | 错误3960 | 重试逻辑,或改回RCSI |

| 撤消(Undo)被阻塞 | 回滚需要锁,但被其他会话阻塞 | 查看wait\_type | 找出阻塞源,或终止回滚(极端) |

| 日志写入瓶颈 | WRITELOG等待过高 | sys.dm\_os\_wait\_stats | 日志盘分离、加速磁盘、优化事务大小 |

#### 8. 最佳实践

1. **理解恢复模式的取舍**:

- 简单模式:最小化管理,但只能恢复到完整备份

- 完整模式:支持时间点恢复,但需要日志备份

- 大容量日志:批量操作时性能最佳,但日志备份含大容量操作

2. **合理设置目标恢复时间**:

```sql

-- 平衡恢复时间与运行时性能

ALTER DATABASE YourDB SET TARGET_RECOVERY_TIME = 60 SECONDS; -- 目标1分钟

```

3. **避免长事务**:

- 不要在事务中等待用户输入

- 将大批量操作分批提交(每批1000-5000行)

- 使用sys.dm\_tran\_active\_transactions监控

4. **行版本控制的适用场景**:

- RCSI:读写混合、要求读取最新已提交数据 → 首选

- 快照隔离:需要事务级别一致性读,且可以接受更新冲突 → 次选

- 两者都避免:大量写入且tempdb空间有限的环境

5. **监控检查点频率**:

```sql

-- 查看检查点相关计数器

SELECT counter_name, cntr_value

FROM sys.dm_os_performance_counters

WHERE counter_name IN ('Checkpoint pages/sec', 'Lazy writes/sec');

```

---

**第八期小结**

事务与并发控制的内部实现基于事务日志(LSN链)和行版本控制(tempdb存储)。恢复过程遵循ARIES模型的三阶段:分析→重做→撤消。行版本隔离(RCSI/快照)通过牺牲tempdb空间换取读写不互斥的优势。理解这些底层机制,就能解释为什么恢复耗时、为什么tempdb暴增、以及如何正确设置恢复模式与检查点策略。

**下期预告**:高可用与灾难恢复 —— 日志传送、复制、Always On可用性组,如何选择合适的高可用架构?