日志与恢复
持久性与原子性都由同一样东西支撑:预写日志(WAL)。本章讲 WAL 的规则、redo/undo、检查点、ARIES 恢复算法,以及 fsync 与组提交的权衡(→ 文件系统)。
WAL 的规则
Section titled “WAL 的规则”预写日志(write-ahead logging)先写日志再写数据——两条铁律:
- 修改数据页之前,对应的日志记录必须已落盘;
- 事务提交之前,全部日志记录必须已落盘(fsync)。
WAL 把随机写变成顺序写:日志是顺序追加的文件(→ I/O、总线与存储设备),数据页是随机的——先把意图记到顺序日志(一次 fsync),数据页留在缓冲池慢慢刷。
WAL 同时买到两样东西:
- 持久性:提交即日志落盘——崩溃后重放日志恢复已提交的事务;
- 原子性:未提交事务的日志带 undo——崩溃后回滚。
redo 与 undo
Section titled “redo 与 undo”- redo(重做):崩溃后重放已提交事务的修改——"你说过要做,就再做一遍";
- undo(回滚):撤销未提交事务的修改——"你没说做完,就当没做过"。
日志记录的三种策略:只 redo、只 undo、两者都记(ARIES 的选择)。** steal/no-force** 的取舍:数据页可以随时刷出(日志保证 redo),提交不必刷数据页(no-force,省 fsync)——这是 WAL 对性能的核心贡献:提交只付一次日志 fsync,数据页的随机写被推迟与合并。
检查点(checkpoint)
Section titled “检查点(checkpoint)”日志无限增长不行(恢复要重放全部历史)。检查点定期把脏页刷盘、记录"此刻的日志位置"——恢复只需从此处重放。检查点是恢复时间与运行时开销的旋钮:太频繁(运行时开销大)、太稀(恢复慢)。
ARIES 恢复算法
Section titled “ARIES 恢复算法”工业标准的恢复算法,三阶段:
- 分析(analysis):从检查点出发,确定崩溃时活跃的事务与脏页;
- redo(重做):从检查点重放全部日志(包括未提交的)——把数据库恢复到崩溃瞬间;
- undo(回滚):倒序回滚未提交事务。
崩溃恢复 = 重放 redo + 回滚 undo。ARIES 的关键设计:重复历史(repeating history)——先把数据库恢复到崩溃时的样子(redo 一切),再回滚未提交的。undo 时产生的操作也写日志(补偿日志),保证"回滚"本身崩溃安全。
提交要 fsync 日志——一次 fsync 毫秒级,高并发下每个事务一次 fsync 是瓶颈。组提交(group commit):多个并发提交合并成一次 fsync——组提交摊薄 fsync 成本,并发越高摊得越薄。它是 WAL 系统的标准优化(MySQL、PostgreSQL、Kafka 都有),代价是每个提交的延迟略增(等同行者)。
与文件系统日志的关系
Section titled “与文件系统日志的关系”文件系统的日志(→ 文件系统)与数据库的 WAL 是同一思想的两层:
- 文件系统日志保证"磁盘元数据一致"(块设备层);
- 数据库 WAL 保证"事务语义"(逻辑层);
- 数据库的 fsync 依赖文件系统日志的正确性——两层日志的正确性链条(文件系统 fsync 语义不可靠时,数据库的持久性承诺也塌,→ fsync 的真实语义)。
- WAL 先写日志再写数据:一次顺序 fsync 买到持久性 + 原子性
- WAL 把随机写变成顺序写;no-force 让提交只付一次日志 fsync
- 崩溃恢复 = 重放 redo + 回滚 undo;ARIES 三阶段:分析、redo、undo
- 检查点是恢复时间与运行开销的旋钮;undo 也要写日志(补偿)
- 组提交摊薄 fsync 成本:并发越高摊得越薄