第三期:事务日志、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 示例):
- 修改缓冲池中的数据页 → 页变脏。
- 同时将修改操作写入日志缓存。
- 事务提交时:强制执行 日志刷盘(
log flush),把日志缓存中的记录写入.ldf。 - 只有日志刷盘成功,客户端才收到“提交成功”。
- 脏页可以稍后由 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_counters | Log 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. 最佳实践与调优
- 分离日志文件到高 IOPS 磁盘(不要放系统盘或数据盘)。
- 预估日志大小,手动设置初始大小和增长增量(如 1~4 GB),避免频繁自动增长。
- 完整恢复模式下定时做日志备份(如每 15~30 分钟),防止日志无限膨胀。
- 避免不必要的长事务(例如事务中等待用户输入或跨网络调用)。
- 使用间接检查点(
TARGET_RECOVERY_TIME)控制脏页刷新频率,平衡恢复时间与性能。 - 监控
log_reuse_wait_desc了解日志截断被什么阻止:
SELECT name, log_reuse_wait_desc FROM sys.databases;
常见等待原因:LOG_BACKUP(未备份)、ACTIVE_TRANSACTION(未提交事务)、REPLICATION 等。
第三期小结
事务日志与 WAL 是 SQL Server 持久性和原子性的基石。理解日志的物理结构(VLF)、刷盘机制、检查点与恢复流程,能帮你解释和解决“重启恢复慢”、“日志暴涨”、“事务无法回滚”等真实问题。核心原则很简单:日志先于数据落盘。架构上牢记这点,后续分析 I/O 瓶颈和灾难恢复时就不会偏离。
下期预告:存储引擎深度(三)—— 堆、聚集索引与非聚集索引的物理存储与访问方式,为什么“索引覆盖”能大幅提速?