数据库存储引擎想要保证宕机之后数据不丢、事务满足 ACID,核心依靠 WAL、LSN 和 ARIES 这套崩溃恢复体系,WAL 也就是预写日志,规定修改数据页之前必须先把日志持久化到磁盘,LSN 是日志里唯一递增编号,用来标记日志和数据页版本,ARIES 则是一套经典的三步恢复算法,就算系统突然断电、进程崩溃,重启之后也能依靠日志把数据库恢复到一致状态。如果不使用 WAL,直接修改磁盘上的数据页,一次事务可能要随机写多个页面,随机 IO 很慢,而且写到一半断电,页面就处于破损、半写状态,数据库直接损坏;有了 WAL 之后,所有改动顺序追加写到日志,日志是顺序写入,速度快很多,再配合 LSN 标记每一条日志、每个数据页的最新版本,ARIES 就可以按照日志做重做、回滚,保证已经提交的事务全部生效,未提交的事务全部撤销。我们理解这套机制的时候,可以分开看懂 WAL 的写入规则、LSN 的传递关系,再顺着 ARIES 的分析、重做、撤销三步走完恢复流程,最后再对照极简 C++ 样例看懂数据流,就能自己动手实现一个具备基础崩溃恢复能力的简易存储引擎。
WAL(预写日志)核心规则清单
- 日志优先:对数据页的任何修改,对应的 WAL 日志必须先刷盘成功,才允许把脏页刷到数据文件,也就是 WAL 协议。
- 顺序追加:日志文件以追加写为主,相比随机更新数据页,大幅降低磁盘 IO 开销。
- 日志类型:包含更新日志、提交日志、回滚日志、检查点日志。
- 持久化选项:可以根据业务选择 fsync 刷盘或者延迟刷盘,权衡性能和安全性。
- 日志归档:旧日志可以归档,配合检查点,减少崩溃恢复时需要扫描的日志范围。
LSN 编号含义对照表
| 字段 | 作用 | 说明 |
|---|---|---|
| 日志 LSN | 每条 WAL 日志的全局唯一递增序号 | 代表这条日志产生的顺序 |
| Page LSN | 数据页头部保存的 LSN | 记录这个页面最后一次修改对应的日志编号 |
| Rec LSN(redo LSN) | 检查点中记录,恢复起点 | 恢复时从该 LSN 开始向后扫描日志 |
| Undo LSN | 回滚链表标记 | 用于 ARIES 的撤销阶段定位日志 |
ARIES 崩溃恢复三大阶段
- 分析阶段(Analysis):从最近一次检查点开始扫描 WAL,构建两张表:事务表(TRX_TABLE)、脏页表(DPT),标记崩溃那一刻哪些事务还没提交、哪些页面是脏页。
- 重做阶段(Redo):顺着日志从 Rec LSN 往后重做所有修改,不管事务最后有没有提交,把所有页面恢复到崩溃前的最新状态,让磁盘页面和日志对齐。
- 撤销阶段(Undo):遍历事务表里崩溃时未提交的事务,反向沿着日志链表回滚它们的修改,消除没有提交的脏数据,最终数据库达到一致性。
极简 C++ 实现核心逻辑
下面是精简版伪实现,只保留 WAL、LSN、ARIES 恢复主干,去掉缓存池、锁、日志归档等复杂模块。
include
include
include
include
using LSN = uint64_t;
enum LogType { UPDATE, COMMIT, ABORT, CHECKPOINT };
struct WalRecord {
LSN lsn;
LogType type;
uint32_t page_id;
std::string before_img;
std::string after_img;
};
class SimpleStorage {
public:
LSN next_lsn = 1;
std::vector wal;
std::fstream wal_file;
LSN append_log(LogType t, uint32_t pid, std::string bef, std::string aft) {
WalRecord r{next_lsn, t, pid, bef, aft};
wal.push_back(r);
persist_wal(r);
return next_lsn++;
}
void persist_wal(const WalRecord& rec) {
wal_file.write((char)&rec.lsn, sizeof(rec.lsn));
wal_file.write((char)&rec.type, sizeof(rec.type));
wal_file.flush();
}
void aries_recovery() {
// 1.分析阶段:重建事务表、脏页表
// 2.重做阶段:回放所有日志更新页面
// 3.撤销阶段:回滚崩溃时未提交事务
}
};
使用流程:每次修改页面前调用 append_log 写入 WAL;崩溃重启调用 aries_recovery() 执行完整恢复流程。
完整运行流程
- 开启事务,修改内存里的数据页。
- 生成对应的 WAL 记录,分配新 LSN,追加写入日志并刷盘。
- 更新内存页面头部 Page LSN,和这条日志 LSN 对齐。
- 事务提交,写入 COMMIT 类型 WAL 日志并刷盘。
- 后台刷脏页,把页面落盘;页面落盘时携带自己的 Page LSN。
- 如果进程崩溃,下次启动执行 ARIES 三段式恢复。
【常见问题】
问题 1:数据库存储引擎里 WAL、LSN 和 ARIES 之间是什么关系?
回答 1:WAL 是底层日志机制,保证修改先写日志;LSN 是 WAL 日志的递增编号,用来标记版本;ARIES 是一套利用 WAL 和 LSN 完成崩溃恢复的算法,三者配合让数据库宕机后可以恢复到一致状态。
问题 2:在 ARIES 崩溃恢复的重做阶段,会不会去重做已经提交事务之外的日志?
回答 2:ARIES 崩溃恢复的重做阶段不区分事务状态,只要日志对应的页面是脏页,就会全部重做,等重做阶段结束之后,再在撤销阶段清理掉崩溃时未提交事务的修改。
问题 3:为什么 WAL 协议要求日志必须在脏页刷盘之前持久化?
回答 3:如果不遵守 WAL 协议,脏页先刷盘而日志丢失,一旦崩溃,LSN 对应的修改没有日志可以用来恢复,就会出现数据损坏、事务一致性被破坏。
问题 4:这个极简 C++ 实现的 WAL、LSN、ARIES 代码和真实 InnoDB 的实现差距在哪?
回答 4:这份极简 C++ 实现只保留 WAL、LSN、ARIES 的主干逻辑,缺少缓冲池、日志组、双写、锁、MVCC、日志归档、异步刷盘,真实 InnoDB 还增加了很多工程优化。
【参考资料】
[2026-11-05](ARIES: 数据库经典崩溃恢复算法原理详解)知乎
[2026-10-28](数据库 WAL、LSN 与崩溃恢复机制)极客时间
[2026-10-15](Aries Recovery Algorithm 论文解读与简易实现)GitHub
[2026-09-20](InnoDB 源码:WAL 日志与 LSN 流转)MySQL 官方文档翻译站
[1992-01-01](ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Rollbacks)ACM
【免责声明】
本文为数据库存储引擎 WAL、LSN、ARIES 原理科普,附带的极简 C++ 示例仅用于教学演示,不能直接用于生产环境;生产级数据库恢复逻辑、刷盘策略、LSN 维护规则会根据引擎(InnoDB、RocksDB、LevelDB)存在差异,文中内容不构成工程落地方案,上线使用前请参考官方论文与源码。
