第三期:事务日志、WAL 与崩溃恢复 —— 如何保证断电也不丢数据?

0 阅读5分钟

第三期:事务日志、WAL 与崩溃恢复 —— 如何保证断电也不丢数据?

1. 事务日志的物理结构:不只是“一个文件”

每个数据库至少有一个日志文件(.ldf),内部被划分为 VLF(Virtual Log File) 逻辑单元:

  • VLF 数量由日志文件大小和自动增长参数决定(过多 VLF 会影响性能)。

  • LSN(Log Sequence Number):每个日志记录的单调递增编号,用于定位和恢复。

  • 日志记录内容:事务开始/结束、每次数据修改(Before/After 或操作描述)、页分配、DDL 操作等。

  • 现象解释:为什么 ALTER DATABASE … MODIFY FILE 后日志仍很大?因为 VLF 已被分配但未截断或收缩。收缩日志会释放尾部未使用的 VLF。

2. 最关键原则:WAL(Write-Ahead Logging)

在将数据页的修改写入磁盘(数据文件 .mdf/.ndf)之前,必须先将对应的日志记录写入磁盘(.ldf)。

  • 为什么?

    • 持久性:事务提交时,即使数据页还在内存中(脏页),日志已经硬化,重启后可通过 Redo 恢复已提交事务。
    • 原子性:未提交事务的修改,可通过 Undo 回滚(基于日志中的“撤消信息”)。
  • 具体流程(UPDATE 示例)

    1. 修改缓冲池中的数据页 → 页变脏。
    2. 同时将修改操作写入日志缓存。
    3. 事务提交时:强制执行 日志刷盘log flush),把日志缓存中的记录写入 .ldf
    4. 只有日志刷盘成功,客户端才收到“提交成功”。
    5. 脏页可以稍后由 Checkpoint/LazyWriter 异步写回。
  • 现象解释:为什么高并发插入/更新时,磁盘 I/O 压力常在日志盘?因为每次提交都要刷日志。使用“延迟提交”或批量提交可缓解。

3. 日志缓存与刷盘机制

  • 日志缓存:每个事务先写到内存中的日志缓存(不可分页),大小固定(通常几十到几百 KB)。

  • 刷盘触发条件

    • 事务提交(最常见)
    • 日志缓存满(约 60KB 或 1/3 缓存)
    • 显式 CHECKPOINT
    • 其他需要强一致性的操作(如创建数据库快照)
  • 刷盘方式:调用 WriteFile + FlushFileBuffers 确保持久化(成本高)。

  • 优化选择

    • DELAYED_DURABILITY(延迟持久性):允许事务提交时不立即刷日志,提升性能但可能丢失最近的数据(适合非关键日志)。

4. 检查点(Checkpoint)与崩溃恢复流程

检查点:将当前所有脏页从缓冲池写回数据文件,并在日志中标记一个 LSN(最老脏页对应的 LSN)。

  • 作用:
    • 缩短崩溃恢复时 需要重做的日志量
    • 避免日志无限增长(但日志截断依赖备份或简单恢复模式中的检查点)。

崩溃恢复三步走(SQL Server 重启或 RESTORE WITH RECOVERY):

1. 分析阶段:扫描日志,找出检查点后所有活跃事务(已提交但未 Redo?实际顺序略有调整)。
2. 重做(Redo):从检查点的最小 LSN 开始,重复所有已提交事务的修改(确保数据页对应日志)。
3. 撤消(Undo):回滚所有未提交事务的修改(基于日志中的撤消信息)。

最终数据库一致。
  • 现象解释
    • 为什么 SQL Server 重启后长时间“正在恢复”?因为需要重做/撤消大量日志(日志文件很大或检查点不频繁)。
    • 为什么大事务回滚也很慢?回滚本质也是重放日志中的“撤消操作”(需要从头到尾扫描日志)。

5. 日志截断、收缩与增长

  • 日志截断逻辑上标记日志中不活动的 VLF 为可重用(并不会释放磁盘空间)。
    触发条件:

    • 简单恢复模式:每次检查点后截断。
    • 完整恢复模式:只有日志备份后才会截断。
  • 日志收缩DBCC SHRINKFILE 可将未使用的尾部 VLF 释放给操作系统,但会引入大量 I/O 和索引碎片,不建议常规使用

  • 日志暴涨的常见原因

    • 长时间未做日志备份(完整恢复模式)。
    • 大事务(如索引重建、批量更新)一次性生成海量日志。
    • 复制/AlwaysOn 同步等待(未确认)。
    • 延迟提交或未提交的打开事务。

6. 监控与诊断关键 DMV

常用视图/函数信息
sys.dm_tran_database_transactions当前事务的日志空间使用量(database_transaction_log_bytes_used
sys.dm_tran_log_stats(2016+)日志统计:每秒写入量、等待 WRITELOG
DBCC LOGINFO查看 VLF 状态(活动/可重用)
sys.dm_os_performance_countersLog Bytes Flushed/sec, Log Flushes/sec
sys.dm_db_log_space_usage总日志大小与已用百分比
  • 关键指标
    • Log Flush Waits/sec 过高 → 日志磁盘瓶颈。
    • Log Growths/sec 持续 >0 → 自动增长频繁(性能杀手),需预分配大小。
    • VLF 数量DBCC LOGINFO 输出行数)>200 说明日志文件被过度自动增长过。

7. 最佳实践与调优

  1. 分离日志文件到高 IOPS 磁盘(不要放系统盘或数据盘)。
  2. 预估日志大小,手动设置初始大小和增长增量(如 1~4 GB),避免频繁自动增长。
  3. 完整恢复模式下定时做日志备份(如每 15~30 分钟),防止日志无限膨胀。
  4. 避免不必要的长事务(例如事务中等待用户输入或跨网络调用)。
  5. 使用间接检查点TARGET_RECOVERY_TIME)控制脏页刷新频率,平衡恢复时间与性能。
  6. 监控 log_reuse_wait_desc 了解日志截断被什么阻止:
SELECT name, log_reuse_wait_desc FROM sys.databases;

常见等待原因:LOG_BACKUP(未备份)、ACTIVE_TRANSACTION(未提交事务)、REPLICATION 等。


第三期小结

事务日志与 WAL 是 SQL Server 持久性和原子性的基石。理解日志的物理结构(VLF)、刷盘机制、检查点与恢复流程,能帮你解释和解决“重启恢复慢”、“日志暴涨”、“事务无法回滚”等真实问题。核心原则很简单:日志先于数据落盘。架构上牢记这点,后续分析 I/O 瓶颈和灾难恢复时就不会偏离。

下期预告:存储引擎深度(三)—— 堆、聚集索引与非聚集索引的物理存储与访问方式,为什么“索引覆盖”能大幅提速?