数据库系列(七):日志与崩溃恢复 — 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 横向对比

维度OracleSQL ServerMySQLPostgreSQL
RedoRedo LogTransaction LogRedo LogWAL
UndoUndo Segment(在 .ldf 中)Undo LogHeap 内(旧 tuple)
归档Archive LogLog BackupBinlogWAL Archive
复制Redo StreamTransaction LogBinlogWAL 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基于时间 + LSNrecovery 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 LogBinlog
层级引擎层(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)。