高级 #database#logging#recovery

日志与恢复(Undo/Redo)

WAL(Write-Ahead Logging)是数据库故障恢复的核心——先写日志再写数据。Undo Log 回滚未提交的事务,Redo Log 重做已提交但未写入磁盘的事务

🔌 最坏的情况——写到一半断电了

你在支付宝上转了 100 元给室友。页面显示”转账成功”——但就在这个时候,支付宝的数据库服务器断电了

你肯定不希望出现这种情况:重启后,你的账户少了 100 元,室友的账户没有增加。或者更糟——你的账户没变,室友多了 100 元。

这就是数据库持久性(Durability)要保证的——系统崩溃后重启,已提交的事务不能丢,未提交的事务不能残留。

实现这个保证的技术叫 WAL(Write-Ahead Logging,预写日志)

📝 类比:Word 的自动恢复

你在写论文,突然断电了。重启后 Word 问:“检测到上次未正常保存的文档,是否恢复?”

你点”是”,论文回来了——虽然最后几分钟的修改可能没来得及保存,但之前保存的内容都在。

Word 是怎么做到的?它在你编辑时会持续写入一个”恢复文件”(相当于 Redo Log)。即使主文件还没更新,恢复文件里已经记录了你的修改。

数据库的 WAL 就是这个”恢复文件”——而且比 Word 做得更彻底、更安全。


📜 WAL 的基本规则

WAL 的规则非常简单,但极其重要:

在修改数据页之前,先把修改记录写到日志文件。

传统流程(不安全):
1. 直接修改数据页 → ⚡ 断电了 → 数据丢失(没来得及写日志)

WAL 流程(安全):
1. 写 Redo Log(记录"将要把 A 的余额改成 900")
2. 确保 Redo Log 已写入磁盘(fsync)
3. 修改数据页(可能在之后才真正落盘)

  只要第 2 步完成了,就算第 3 步还没来得及做就断电了——重启后也能恢复

为什么 WAL 比直接写数据快?

你可能觉得”先写日志再写数据”多了一步——不是更慢吗?

实际上它更快,原因有二:

  1. 日志是顺序写——日志文件是追加写入的,顺序 I/O 比随机 I/O 快得多(磁盘的顺序写 vs 随机写,速度差距可达 10-100 倍)
  2. 数据页是随机写——数据分布在磁盘的不同位置,写数据是随机 I/O,慢得多
操作写入方式速度
写日志(WAL)追加到文件末尾(顺序写)⚡ 快
写数据页写到磁盘随机位置(随机写)🐢 慢

所以 WAL 的流程是:先把修改快速记录到日志(顺序写),然后再慢慢把数据刷到磁盘(随机写)。


🔴 Redo Log —— 保证”提交了就不能丢”

Redo Log(重做日志) 记录的是”修改后的值”——用于在崩溃后恢复已提交但还没写入磁盘的事务。

Redo Log 中记录了什么

Redo Log 示例(简化格式):
[LSN 1001] T1, UPDATE accounts SET balance=900 WHERE id=1
[LSN 1002] T1, UPDATE accounts SET balance=2100 WHERE id=2
[LSN 1003] T1, COMMIT
[LSN 1004] T2, UPDATE accounts SET balance=500 WHERE id=3

崩溃恢复时的 REDO 阶段

假设系统在第 1005 条日志之后崩溃了。重启后的恢复过程:

  1. 扫描 Redo Log,找出所有已提交的事务(有 COMMIT 标记的)
  2. 对每个已提交的事务,检查它的修改是否已写入数据页
  3. 如果没写 → 重做(Redo)修改
  4. 如果已经写了 → 跳过(不需要重复重做)
# Redo 阶段伪代码
for each log_entry in Redo_Log:
    if log_entry.transaction is committed:
        if log_entry.change not yet applied to data page:
            apply(log_entry.change)  # 把修改应用到数据页

💡 重要理解:Redo 操作是幂等的(Idempotent)——同一个修改做一次和做多次效果一样。所以不需要担心”会不会重复应用了两次修改”。


🟢 Undo Log —— 保证”没做完就能回滚”

Undo Log(撤销日志) 记录的是”修改前的值”——用于回滚未提交的事务。

Undo Log 中记录了什么

Undo Log 示例(简化格式):
T1, UPDATE accounts SET balance=1000→900 WHERE id=1
T1, UPDATE accounts SET balance=2000→2100 WHERE id=2
T2, UPDATE accounts SET balance=800→500 WHERE id=3

每条记录了”改前值”和”改后值”。

崩溃恢复时的 UNDO 阶段

假设系统崩溃时,事务 T2 已经修改了数据但还没提交:

  1. 扫描 Undo Log,找出所有未提交的事务
  2. 对每个未提交的事务,把修改过的数据恢复成”改前值”
# Undo 阶段伪代码
for each log_entry in Undo_Log:
    if log_entry.transaction is NOT committed:
        restore_before_image(log_entry)  # 恢复修改前的值

这样就保证了原子性——未提交的事务就像没发生过一样。

⛑️ 类比:大楼里的灭火系统

Redo Log = 大楼的火警自动喷水系统。火灾后(崩溃后),系统会自动启动,把被烧毁的(丢失的)东西恢复回来。

Undo Log = 大楼的”装修复原”要求。装修时你改了房间布局(未提交的事务),离开时必须恢复原样。

两种日志各自负责一个 ACID 特性:

  • Redo Log → 持久性(D):已提交的修改不丢失
  • Undo Log → 原子性(A):未提交的修改可回滚

🗺️ ARIES 恢复算法

ARIES(Algorithms for Recovery and Isolation Exploiting Semantics)是工业标准的数据恢复算法,被 MySQL InnoDB、PostgreSQL、Oracle 等广泛采用。

ARIES 将崩溃恢复分为三个阶段:

第 1 阶段:分析(Analysis)

扫描日志,确定恢复的”起点”:

扫描日志 → 找出所有事务的状态:
  ┌─ T1: 已提交 → 需要 REDO
  ├─ T2: 未提交 → 需要 UNDO
  └─ T3: 正在运行(没有 COMMIT/ROLLBACK 标记)→ 需要 UNDO

同时找到"脏页表"(Dirty Page Table)——哪些数据页已经被修改但可能还没落盘。

第 2 阶段:REDO(重做)

从日志中的检查点开始,按时间顺序重做所有已提交的修改:

从检查点 LSN 500 开始,正向扫描日志 →
遇到 T1: UPDATE id=1 → 检查数据页是否已经包含了这个修改
   → 如果没包含 → 重做
   → 已经包含 → 跳过
...

一直重做到日志末尾。

第 3 阶段:UNDO(回滚)

反向扫描日志,回滚所有未提交的事务:

从日志末尾开始,反向扫描 →
遇到 T2: UPDATE id=3, balance=800→500
   → 把 id=3 的 balance 恢复成 800

继续反向扫描 →
遇到 T3: UPDATE id=1, balance=1000→900
   → 把 id=1 的 balance 恢复成 1000
...

💡 为什么要反向扫描? 因为事务的修改可能是嵌套的——正向扫描的话,你可能先回滚了一个后续操作又被覆盖的值。反向扫描可以保证”回到最早的状态”。


🏁 检查点(Checkpoint)

如果每次崩溃恢复都要扫描整个日志——对于运行了几个月的数据库来说,日志可能有几百 GB——恢复会非常慢。

检查点(Checkpoint) 就是为了解决这个问题的:定期把内存中的脏页刷入磁盘,并记录一个”截止点”。

日志时间线:
│───────│───────│───────│────────│→
LSN 100  LSN 500  LSN 900  LSN 1500

              检查点:所有在 LSN 900 之前的修改都已写入磁盘

检查点的作用

  • 恢复时不需要扫描检查点之前的日志——因为那些修改已经写入数据页了
  • 恢复只需要从最近的检查点开始扫描
-- MySQL 中手动触发检查点
FLUSH LOGS;

-- 查看最近检查点信息
SHOW ENGINE INNODB STATUS;

🎯 完整的崩溃恢复流程

系统崩溃 → 重启 → 数据库进入"恢复模式"


1. 检查点定位
   └→ 找到最近的检查点,标记恢复起点


2. 分析阶段
   └→ 确定哪些事务已提交(需要 REDO)
   └→ 确定哪些事务未提交(需要 UNDO)


3. REDO 阶段
   └→ 重做所有已提交但未落盘的修改
   └→ 保证持久性(D)


4. UNDO 阶段
   └→ 回滚所有未提交的事务
   └→ 保证原子性(A)


恢复完成 → 数据库开始正常服务

📝 小结

概念一句话
WAL(Write-Ahead Logging)先写日志再写数据——崩溃恢复的基础
Redo Log记录修改后值——保证已提交事务不丢失(持久性)
Undo Log记录修改前值——保证未提交事务可回滚(原子性)
ARIES工业标准恢复算法——三个阶段:分析、REDO、UNDO
检查点(Checkpoint)定期刷脏页到磁盘,减少恢复时需扫描的日志量
幂等性Redo 操作多次执行结果不变——恢复安全的基础

🎯 思考题:为什么检查点不能”每一秒”都执行一次?提示:检查点执行时要把内存中的脏页写入磁盘,这期间对性能有什么影响?

为什么先学这个? 恢复机制是一个数据库”最后一道防线”——理解了它,你对数据库可靠性的理解就完整了。最后,让我们看看更广泛的数据库生态——NoSQL 数据库概述