日志与恢复(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 比直接写数据快?
你可能觉得”先写日志再写数据”多了一步——不是更慢吗?
实际上它更快,原因有二:
- 日志是顺序写——日志文件是追加写入的,顺序 I/O 比随机 I/O 快得多(磁盘的顺序写 vs 随机写,速度差距可达 10-100 倍)
- 数据页是随机写——数据分布在磁盘的不同位置,写数据是随机 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 条日志之后崩溃了。重启后的恢复过程:
- 扫描 Redo Log,找出所有已提交的事务(有 COMMIT 标记的)
- 对每个已提交的事务,检查它的修改是否已写入数据页
- 如果没写 → 重做(Redo)修改
- 如果已经写了 → 跳过(不需要重复重做)
# 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 已经修改了数据但还没提交:
- 扫描 Undo Log,找出所有未提交的事务
- 对每个未提交的事务,把修改过的数据恢复成”改前值”
# 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 数据库概述。