数据库系列(七):日志与崩溃恢复 — ARIES、WAL、Checkpoint

写在前面

承接前几篇,本文聚焦"日志与崩溃恢复"——这是数据库 D(持久性)的实现核心。

本文要回答:

Redo Log、Undo Log、Binlog、WAL、Archive Log、Transaction Log——这么多日志都是干嘛的?数据库崩溃后是怎么自动恢复的?为什么 Oracle 重启很快但 MySQL 重启有时很慢?


一、ARIES 协议

1.1 数据库恢复的理论基础

ARIES(Algorithm for Recovery and Isolation Exploiting Semantics)是 1992 年 IBM 提出的恢复算法,所有现代关系数据库都基于它(变体)。

1.2 三阶段恢复

1
2
3
4
5
6
7
8
ARIES 三阶段:
  1. Analysis(分析):扫描日志,重建事务表 + 脏页表
  2. Redo(重做):重放所有"已提交但未刷盘"的修改
  3. Undo(回滚):回滚所有"未提交但已写盘"的修改

关键概念:
  - LSN(Log Sequence Number):日志序号,全局递增
  - WAL 原则:数据页刷盘前,对应日志必须先刷盘

1.3 完整流程示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
时间线:
  T1: BEGIN
  T1: UPDATE balance SET v = 100 WHERE id = 1;
      - 写 Undo Log: id=1, old_v = 50
      - 写 Redo Log: id=1, new_v = 100
  T2: BEGIN
  T2: UPDATE balance SET v = 200 WHERE id = 2;
      - 写 Undo Log: id=2, old_v = 80
      - 写 Redo Log: id=2, new_v = 200
  T1: COMMIT
      - 写 COMMIT Log
      - fsync Redo Log
  [崩溃]

重启后:
  Analysis:
    - T1 已提交(看到 COMMIT)
    - T2 未提交(没有 COMMIT)

  Redo(从某个 LSN 开始):
    - 重做 T1: id=1 改成 100
    - 重做 T2: id=2 改成 200

  Undo:
    - 回滚 T2: 用 Undo Log 改回 id=2 = 80

  最终:
    - id=1: 100(T1 已提交)
    - id=2: 80(T2 已回滚)

二、Redo Log vs Undo Log

2.1 物理日志 vs 逻辑日志

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
物理日志(Physical Log):
  - 记录"页 X 偏移 Y 改成值 Z"
  - Redo Log 都是物理日志(或物理到逻辑混合)
  - 重放时精确还原页面

逻辑日志(Logical Log):
  - 记录"事务 T 在表 t 改了什么"(行级、SQL 级)
  - Undo Log 多为逻辑日志
  - Binlog 是逻辑日志

为什么 Redo 必须物理?
  - 重放时不能假设页处于某种状态
  - 物理日志幂等(重复执行结果一致)

2.2 Redo Log 作用

1
2
3
4
5
6
7
8
1. 崩溃恢复:重做未刷盘的已提交事务
2. 加速 COMMIT:顺序写 Redo 比"刷数据页"快得多
3. 支持 PITR(Point-in-Time Recovery,配合归档日志)

特点:
  - 物理日志
  - 顺序追加(部分循环覆盖)
  - 大小固定(一组文件)

2.3 Undo Log 作用

1
2
3
4
5
6
7
8
1. 事务回滚:失败时撤销修改
2. MVCC:保留旧版本供并发读
3. 崩溃恢复:回滚未提交事务

特点:
  - 逻辑日志("改回什么值")
  - 通常顺序追加
  - 大小动态(不固定)

三、四大数据库的日志体系

3.1 Oracle

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
日志类型:
  - Redo Log(在线 + 归档)
  - Undo Segment(独立表空间)

文件结构:
  online redo log:3+ 组,每组 1+ 文件,循环写
    redo01.log → redo02.log → redo03.log → 覆盖 redo01.log ...

  archive log:归档模式(ARCHIVELOG)下,被覆盖前复制到归档位置

关键参数:
  LOG_BUFFER:Redo Log Buffer 大小
  LOG_ARCHIVE_DEST_n:归档目的地
  FAST_START_MTTR_TARGET:崩溃恢复目标时间

3.2 SQL Server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
日志类型:
  - Transaction Log(.ldf 文件)
  - 同时承担 Redo 和 Undo(没有独立 Undo Log)

特点:
  - 单一日志(不像其他三家分开)
  - 通过版本号在日志中区分 Redo/Undo 部分
  - 数据库每个都有独立的 .ldf

工作流程:
  1. 修改数据 → 写 Log Buffer
  2. COMMIT → flush Log Buffer 到 .ldf
  3. 后台 Checkpoint 刷数据页
  4. 崩溃恢复:扫 .ldf 重做 / 回滚

关键参数:
  recovery interval:目标恢复时间
  target_recovery_time:每库目标恢复时间(秒)

3.3 MySQL InnoDB

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
日志类型:
  - Redo Log(ib_redo*,8.0+ 改名)
  - Undo Log(undo tablespace)
  - Binlog(Server 层,独立于引擎)

为什么要三套?
  - Redo + Undo:引擎内部崩溃恢复
  - Binlog:主从复制 + 备份
  - 内部 XA 协调 Redo 和 Binlog

工作流程(详见第 4 篇):
  1. 改 Buffer Pool
  2. 写 Undo Log
  3. PREPARE Redo Log(fsync)
  4. 写 Binlog(fsync)
  5. COMMIT Redo Log(fsync)

关键参数:
  innodb_flush_log_at_trx_commit:Redo 刷盘策略(0/1/2)
  sync_binlog:Binlog 刷盘策略(0/1/N)
  innodb_log_file_size:单个 Redo Log 文件大小
  innodb_log_files_in_group:Redo Log 文件数

3.4 PostgreSQL

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
日志类型:
  - WAL(Write-Ahead Log,pg_wal/ 目录)
  - 没有 Undo Log(多版本直接存在 Heap)

特点:
  - 单一日志(WAL)
  - 物理日志为主(8.0+ 部分逻辑)
  - 8.0+ 支持 logical replication(基于逻辑日志)

工作流程:
  1. 改 Buffer(标记脏页)
  2. 写 WAL Buffer
  3. COMMIT → fsync WAL Buffer
  4. 后台 Checkpoint 刷脏页

关键参数:
  wal_level:minimal / replica / logical
  synchronous_commit:on / off / remote_write / remote_flush
  max_wal_size:WAL 大小上限(自动 checkpoint)
  wal_keep_size:为流复制保留的 WAL 大小

3.5 横向对比

维度 Oracle SQL Server MySQL PostgreSQL
Redo Redo Log Transaction Log Redo Log WAL
Undo Undo Segment (在 .ldf 中) Undo Log Heap 内(旧 tuple)
归档 Archive Log Log Backup Binlog WAL Archive
复制 Redo Stream Transaction Log Binlog WAL Stream

四、Checkpoint

4.1 为什么需要 Checkpoint

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
没有 Checkpoint 的问题:
  - 崩溃恢复时,需要从头扫描所有 Redo Log
  - Log 文件越来越大 → 恢复时间越来越长
  - 不可能接受

Checkpoint 的作用:
  - 定期把所有脏页刷盘
  - 在 Redo Log 中记录"Checkpoint LSN"
  - 崩溃恢复时从 Checkpoint LSN 开始扫
  - 缩短恢复时间(MTTR)

4.2 工作机制

1
2
3
4
5
6
7
8
9
Checkpoint 流程:
  1. 记录当前 Redo Log LSN(记为 C)
  2. 把 Buffer Pool 中所有"LSN ≤ C"的脏页刷盘
  3. 在 Redo Log 中写入 Checkpoint 记录
  4. 更新控制文件中的"最新 Checkpoint LSN"

崩溃恢复:
  - 从 Checkpoint LSN 开始扫 Redo Log
  - 之前的数据已经持久化,不需要重做

4.3 各家的 Checkpoint

数据库 触发机制 关键参数
Oracle 基于时间(MTTR) FAST_START_MTTR_TARGET、FAST_START_IO_TARGET
SQL Server 基于时间 + LSN recovery interval、indirect checkpoint
MySQL InnoDB 基于 LSN 上限 innodb_max_dirty_pages_pct、innodb_io_capacity
PostgreSQL 基于 WAL 大小 + 时间 max_wal_size、checkpoint_timeout
1
2
3
4
5
6
7
-- MySQL:控制 Checkpoint 速度
SET GLOBAL innodb_max_dirty_pages_pct = 75;
SET GLOBAL innodb_io_capacity = 2000;

-- PostgreSQL:调整 Checkpoint 频率
ALTER SYSTEM SET max_wal_size = '4GB';
ALTER SYSTEM SET checkpoint_timeout = '15min';

4.4 Fuzzy Checkpoint

1
2
3
4
5
6
7
8
9
现代数据库都用"Fuzzy Checkpoint":
  - 不停服
  - 不冻结写操作
  - 后台慢慢刷脏页
  - 用户无感

代价:
  - 需要"两阶段 Checkpoint"(开始/结束 LSN)
  - 恢复时小范围可能重复 Redo(可接受)

五、崩溃恢复全过程

5.1 通用流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
1. 读取控制文件,找到最新 Checkpoint LSN
2. 从该 LSN 开始扫 Redo Log
   - 重建事务表(哪些事务 BEGIN / COMMIT / ROLLBACK)
   - 重建脏页表(哪些页被修改过)
3. Redo 阶段
   - 重放 Checkpoint 之后的所有修改
   - 已提交 + 未提交都重做(保证页面状态)
4. Undo 阶段
   - 扫描未提交事务的 Undo Log
   - 回滚这些事务的修改
5. 数据库进入正常状态,开始接受连接

5.2 影响恢复时间的因素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
1. Checkpoint LSN 到崩溃时刻的距离
   - 距离越远,Redo 量越大
   - 调小 Checkpoint 间隔可以缩短恢复时间

2. 未提交事务的数量
   - 长事务多 → Undo 量大
   - 长事务是恢复性能杀手

3. 磁盘随机 I/O 性能
   - 恢复涉及大量随机读(读脏页)
   - SSD 比 HDD 恢复快 10~100 倍

4. 并行恢复能力
   - Oracle:多线程并行 Redo / Undo
   - PG:单线程,慢
   - MySQL:InnoDB 8.0+ 部分并行

5.3 实战:恢复时间监控

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- Oracle:看上次恢复的预估时间
SELECT estimated_mttr FROM v$instance_recovery;

-- MySQL:InnoDB 恢复日志
-- 在 mysqld 启动日志中查看:
-- "InnoDB: Doing recovery: scanned X log"
-- "InnoDB: Database was not shutdown normally"

-- PostgreSQL:在启动日志中
-- "starting archive recovery"
-- "redo starts at X"
-- "redo done at Y"
-- "recovery stopping after commit timestamp Z"

六、刷盘策略与"双一"标准

6.1 刷盘时机

1
2
3
4
5
每次写文件系统的"写"操作其实分两步:
  1. 写 OS Page Cache(write())
  2. 刷到磁盘(fsync())

数据库的"刷盘"通常指 fsync()。

6.2 MySQL 的双一标准

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1

含义:
  - innodb_flush_log_at_trx_commit = 1
    每次 COMMIT  fsync Redo Log  D(持久性)保证
  - sync_binlog = 1
    每次事务都 fsync Binlog  复制一致性保证

为什么叫"双一"
  - 两个参数都 = 1
  - 金融场景必须
  - 性能代价:每事务多 1~2  fsync

6.3 不同级别权衡

1
2
3
4
5
6
7
8
9
innodb_flush_log_at_trx_commit:
  0 = 每秒 fsync(崩溃丢最多 1 秒事务)→ 性能好
  1 = 每次 fsync(永不丢)→ 默认、最安全
  2 = 每次 COMMIT 写 OS Cache,每秒 fsync(OS 崩溃丢,DB 崩溃不丢)

sync_binlog:
  0 = 由 OS 决定刷盘时机
  1 = 每次事务都 fsync → 默认(5.7+)
  N = 每 N 个事务 fsync 一次

6.4 PostgreSQL 的对应

1
2
3
4
5
6
synchronous_commit = on    -- 等于 innodb_flush_log_at_trx_commit = 1
synchronous_commit = off   -- 等于 0
synchronous_commit = local -- 类似 1(单机场景)

wal_sync_method = fsync    -- 刷盘方法
wal_writer_delay = 200ms   -- WAL Writer 后台刷盘间隔

七、Binlog 详解

7.1 Binlog 是什么

1
2
3
4
5
6
7
8
MySQL Server 层的逻辑日志
  - 记录"所有已提交事务的修改"
  - 用于主从复制、PITR、CDC(变更数据捕获)

Binlog 格式:
  - STATEMENT:记录 SQL 语句(小,但有些函数 / NOW() 不安全)
  - ROW:记录每行的修改(大,但安全)  ← 推荐
  - MIXED:自动选择

7.2 Binlog vs Redo Log

维度 Redo Log Binlog
层级 引擎层(InnoDB) Server 层
内容 物理日志(页/偏移) 逻辑日志(行变更)
写入 循环覆盖 追加写,文件按序号
用途 崩溃恢复 复制、备份、CDC
大小 固定(几个 G) 一直增长

7.3 为什么需要内部 XA

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
场景:Redo Log 已写但 Binlog 还没写时崩溃
  - 重启后:InnoDB 看到 PREPARE 状态,回滚事务
  - 但如果该事务的 Redo 已经被从库消费(不可能,因为还没 COMMIT)...

实际场景:Redo 写完、Binlog 写了一半时崩溃
  - 重启后:扫 Redo 找到 PREPARE 事务
  - 检查 Binlog 是否完整:
    * 完整 → 重做(保证从库能消费)
    * 不完整 → 回滚(保证与从库一致)

→ 内部 XA 协议保证 Redo + Binlog 的一致性

八、归档与 PITR

8.1 为什么需要归档

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Redo Log / WAL 是循环写的,会被覆盖
  - 在线 Redo 容量有限(通常几个 G)
  - 几小时前的修改可能已经丢失

归档(Archive):
  - 把"将要被覆盖"的 Redo / WAL 复制到归档目录
  - 归档文件永久保存(直到备份策略清理)
  - 用于:
    * 物理备份恢复到任意时间点(PITR)
    * 增量备份基础
    * 流复制从库延迟追赶

8.2 各家的归档

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Oracle:Archive Log
  - 数据库必须开 ARCHIVELOG 模式
  - 后台进程 ARCn 自动归档
  - RMAN 备份 / 恢复工具

SQL Server:Transaction Log Backup
  - 数据库必须 FULL 或 BULK_LOGGED 恢复模式
  - 主动执行 BACKUP LOG 命令

MySQL:Binlog(自动归档)
  - 默认 binlog 一直保留
  - binlog_expire_logs_seconds 控制保留时间

PostgreSQL:WAL Archive
  - archive_mode = on
  - archive_command = 'cp %p /backup/%f'
  - 11+ 支持 replication slot + 物理备份

8.3 PITR(Point-in-Time Recovery)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
场景:今天 14:00 误删了重要表,需要恢复到 13:59:59

流程:
  1. 用一周前的全量备份恢复(base restore)
  2. 应用归档日志,重放到目标时间点
  3. 用 RESETLOGS / 类似命令打开数据库

各家命令:
  Oracle:RMAN RECOVER DATABASE UNTIL TIME "TO_DATE('2026-06-15 13:59:59')"
  SQL Server:RESTORE LOG ... WITH STOPAT='2026-06-15 13:59:59'
  MySQL:mysqlbinlog --stop-datetime='2026-06-15 13:59:59' binlog.* | mysql
  PostgreSQL:recovery_target_time = '2026-06-15 13:59:59'

九、小结

本文学习了日志与崩溃恢复:

  • ARIES 协议:Analysis → Redo → Undo 三阶段
  • 物理日志(Redo)vs 逻辑日志(Undo/Binlog)
  • 四大数据库的日志体系差异
  • Checkpoint 机制与 Fuzzy Checkpoint
  • 崩溃恢复全过程(影响 MTTR 的因素)
  • MySQL 的"双一"标准(持久性 + 复制一致性)
  • Binlog 与 Redo Log 的内部 XA 协调
  • 归档与 PITR
1
2
3
4
记住三句话:
  1. WAL 是数据库持久性的根基——数据页刷盘前必须先写日志
  2. Checkpoint 是恢复时间的"分水岭"——之前的不重做
  3. MySQL 同时有 Redo 和 Binlog 是历史包袱,但要靠内部 XA 保证一致

下一篇进入复制与高可用:MySQL MGR、PostgreSQL 流复制、SQL Server AlwaysOn、Oracle ADG/RAC,以及共识协议(Paxos / Raft)。