在上一篇文章中,我们认识了 Redo Log、Undo Log 和双写缓冲区这三大"守护神"。它们各自在内存和磁盘之间扮演着重要角色,但直到系统真正崩溃并重启的那一刻,它们才会携手完成一场华丽的"恢复表演"。那么,MySQL 在意外宕机后到底是如何自动恢复数据的?为什么已提交的事务绝不会丢失?我们又该如何验证这一切?
本文将带你深入 MySQL 崩溃恢复的内部流程,从 LSN 与 Checkpoint 的协作机制讲起,然后拆解恢复的三个阶段(分析、重做、回滚),最后亲手模拟一次宕机并亲眼观察恢复过程。
读完本文,你将能够:
- 理解 LSN 和 Checkpoint 如何界定"需要恢复的日志范围"
- 掌握崩溃恢复的三阶段流程
- 明白
innodb_flush_log_at_trx_commit等参数如何影响持久性 - 通过实战验证 Redo Log 的保护能力
1. 恢复的起点:LSN 与 Checkpoint
恢复的核心问题是:从哪一条日志开始重做?
1.1 LSN 追踪一切变化
LSN(Log Sequence Number)是一个 8 字节的单调递增数字,代表 Redo Log 的累计写入量。它不仅出现在日志文件中,还存储在:
- 每个数据页的头部 (
FIL_PAGE_LSN):表示"最后一个修改本页的日志的 LSN"。 - Checkpoint 信息:记录"到这个 LSN 为止的所有脏页都已安全刷盘"。
这意味着,通过比较日志的 LSN 和数据页的 LSN,MySQL 就能知道:
- 若 页 LSN < 日志 LSN:该页的修改还未写入磁盘,需要用日志重做。
- 若 页 LSN >= 日志 LSN:该页已经包含了这次修改,无需再重做。
1.2 Checkpoint 划出的安全线
Checkpoint 是一个"截止点",它标志着:"在此之前的 Redo Log 对应的所有脏页都已落盘"。所以,恢复时只需要从最后一次 Checkpoint 之后的日志开始扫描。
Redo Log 环形空间:
[已刷盘且可覆盖] | [需要恢复的区间] | [空闲空间]
↑ ↑
Checkpoint LSN Latest LSN
- Checkpoint 之前的日志:对应的数据页已经安全落在磁盘上,恢复时忽略。
- Checkpoint 之后的日志:脏页可能还没刷盘,这部分日志必须用于恢复。
InnoDB 在运行时会不断推进 Checkpoint,以释放 Redo Log 空间。正常关闭时执行一次 Sharp Checkpoint(所有脏页全刷),重启时几乎没有恢复工作。而意外崩溃时,最近一次 Checkpoint 之后的日志就定义了恢复的工作量。
2. 崩溃恢复的三个阶段
当 MySQL 意外关闭(如断电、kill -9)后再次启动,InnoDB 会自动执行崩溃恢复。整个过程可以分为三个阶段:
2.1 阶段一:分析(Analysis)
目标:确定从哪个 LSN 开始恢复,并扫描日志,找出所有包含"未完成操作"的页。
具体步骤:
- 从 Checkpoint 信息中找到最后一次 Checkpoint 的 LSN(
checkpoint_lsn)。 - 从该 LSN 开始,顺序扫描 Redo Log 文件,直到末尾。
- 解析每条日志,记录下被修改的页号 和操作类型。这些信息会被存入一个哈希表(脏页表)。
这一步只是为了收集信息,不会修改任何数据页。扫描完毕后,InnoDB 就知道:
- 有哪些页在崩溃时可能处于"不完整"状态。
- 哪些事务在崩溃时还是活跃的(未提交)。
2.2 阶段二:重做(Redo)
目标 :将 Redo Log 中记录的所有修改重新应用到数据页上,无论事务是否提交。
具体步骤:
- 再次从
checkpoint_lsn开始扫描 Redo Log,一直扫到日志末尾。 - 对于每条日志,检查其对应的数据页的
FIL_PAGE_LSN:- 如果 页 LSN < 日志 LSN:说明这次修改尚未写入磁盘,需要从磁盘读取该页,应用 Redo Log 中的修改,再将页写回磁盘。
- 如果 页 LSN >= 日志 LSN:说明页已经包含了这次修改(可能在崩溃前刚好刷盘了),跳过。
- 全部日志扫描完成后,所有已提交和未提交的修改都已被反映到磁盘的数据页中。
为什么连未提交的修改也要重做?
因为未提交的事务可能在崩溃前已经把部分脏页刷到了磁盘(由后台刷盘线程所为),如果只重做已提交的日志,就会导致磁盘上的数据页出现"半成品"状态。所以必须全部重做,让所有页都达到崩溃瞬间的最新状态,之后再统一回滚未提交的事务。
2.3 阶段三:回滚(Undo)
目标:找出崩溃时所有未提交的事务,利用 Undo Log 回滚它们。
具体步骤:
- 扫描 Undo Log(活跃事务表),找出所有在崩溃时处于
ACTIVE或PREPARED状态的事务。 - 对于每个未提交的事务,沿着它的 Undo 版本链,逆向执行 Undo Log 中记录的操作(INSERT → DELETE,UPDATE → 恢复旧值)。
- 回滚完成后,这些事务的修改被彻底抹除,数据恢复到一致状态。
2.4 为什么已提交的事务绝不会丢失?
关键在于 Redo Log 先于数据页持久化 的 WAL 规则:
- 当事务提交时,对应的 Redo Log 必须已经写入磁盘(由
innodb_flush_log_at_trx_commit参数控制)。 - 即使数据页还留在内存的 Buffer Pool 中未刷盘,只要 Redo Log 落盘了,崩溃恢复时就能通过重做还原出已提交的数据。
- 这就像你先把要做的事记在纸上(Redo Log),即使你突然失忆,看着纸条也能把事做完。
参数警示:
innodb_flush_log_at_trx_commit = 1(默认):每次提交都刷 Redo Log 到磁盘,绝对持久。= 0:每秒刷一次,崩溃可能丢失最近 1 秒的事务。= 2:提交时写日志到 OS 缓存,每秒刷盘,断电可能丢失最近 1 秒数据,但 MySQL 进程崩溃不会丢。
3. 与 Doublewrite Buffer 的联动
在恢复的重做阶段,如果发现某个数据页的校验和错误(页断裂),InnoDB 会尝试从 Doublewrite Buffer 中恢复该页的完整副本。具体流程:
- 读取数据页时,计算校验和,与页尾的校验值比对。
- 如果不匹配,说明发生了页断裂(写入时断电)。
- InnoDB 查找 Doublewrite Buffer(系统表空间中连续 128 页),看是否有该页的完整副本。
- 若有,用副本覆盖损坏页,再继续应用 Redo Log。
- 若没有(极少情况),恢复失败,可能需要从备份恢复。
因此,Doublewrite 为数据页的完整性提供了一层"保险",确保恢复时的基础页面没有物理损坏。
4. 实战:模拟宕机并观察恢复过程
我们来手动制造一次"意外宕机",然后观察 MySQL 重启后如何自动恢复。
4.1 准备环境
sql
CREATE DATABASE IF NOT EXISTS crash_test;
USE crash_test;
CREATE TABLE crash_me (
id INT PRIMARY KEY,
value INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO crash_me VALUES (1, 100);
4.2 开启事务,制造未提交的修改
会话 A:
sql
START TRANSACTION;
UPDATE crash_me SET value = 200 WHERE id = 1;
-- 注意:此时未提交!
保持这个会话不要关闭,我们接下来会强制关闭 MySQL。
4.3 模拟宕机
在操作系统终端,找到 MySQL 的进程 ID 并强制终止(相当于断电):
bash
# 查看 MySQL 进程
ps aux | grep mysqld
# 强制杀死(假设 PID 为 12345)
sudo kill -9 12345
或者,如果你用的是 Systemd:
bash
sudo systemctl kill -s SIGKILL mysqld
警告 :仅在实验环境执行,生产环境严禁
kill -9!
4.4 重启 MySQL 并查看结果
bash
sudo systemctl start mysqld
待 MySQL 启动完成后,重新连接:
sql
USE crash_test;
SELECT * FROM crash_me;
结果预测 :value 仍然是 100(未提交的事务被回滚了)。
如果我们在崩溃前提交了事务:
sql
-- 重做实验
UPDATE crash_me SET value = 300 WHERE id = 1;
COMMIT;
-- 然后立即 kill -9(在提交刷盘完成后立刻杀)
重启后再查,value 应为 300(已提交,通过 Redo Log 恢复)。
4.5 观察恢复日志
在 MySQL 错误日志中(通常位于 /var/log/mysql/error.log 或 /var/lib/mysql/hostname.err),你会看到崩溃恢复的痕迹:
InnoDB: Doing recovery: scanned up to log sequence number ...
InnoDB: Starting an apply batch of log records to the database...
InnoDB: Apply batch completed
...
InnoDB: Last MySQL binlog file position ...
InnoDB: 1 transaction(s) which must be rolled back or cleaned up
InnoDB: Starting rollback of uncommitted transactions...
关键信息:
scanned up to log sequence number------ 扫描到的最大 LSNapply batch------ 正在重做rollback of uncommitted transactions------ 正在回滚未提交事务
4.6 清理
sql
DROP DATABASE crash_test;
5. 恢复时间与调优
恢复时间主要取决于需要重做的日志量 (Latest LSN - Checkpoint LSN)。这个差值越大,需要扫描和应用的日志就越多,启动就越慢。
控制恢复时间的策略:
- 合理设置 Redo Log 大小 :增大
innodb_log_file_size可以降低 Checkpoint 频率,但恢复时间会变长。一般建议使日志切换时间保持在 20~60 分钟。 - 缩短 Checkpoint 间隔 :
innodb_io_capacity等参数影响后台刷盘速度,适当提高可以加快 Checkpoint 推进,但会增加 I/O 压力。 - 监控恢复窗口 :通过
SHOW ENGINE INNODB STATUS中的LOG段观察 LSN 与 Checkpoint 的差值。
6. 小结
崩溃恢复是 InnoDB 保证数据一致性的最后一道防线,它将 Redo Log、Undo Log、LSN、Checkpoint 和 Doublewrite Buffer 精密地编织在一起:
- LSN 是全系统的时间线,Checkpoint 是恢复的起点。
- 恢复三阶段:分析 (扫描日志,收集脏页)→ 重做 (应用所有修改,不论是否提交)→ 回滚(撤销未提交事务)。
- 已提交的数据依赖于"先写日志后写数据"的 WAL 规则,只要 Redo Log 落盘,数据就永久安全。
- 双写缓冲区在恢复时修复可能出现的页断裂。
- 可以通过合理配置 Redo Log 大小和刷盘策略,在性能与恢复时间之间取得平衡。
通过亲手模拟宕机,我们亲眼看到未提交的数据被回滚,已提交的数据被恢复------这比任何文字描述都更有说服力。
至此,第四阶段第 6 篇完成。下一篇我们将迎来第四阶段的最后一篇------【实战】分析一张真实业务表的 InnoDB 存储结构 ,届时我们将使用工具实际解析 .ibd 文件,将前面六篇文章的理论知识一网打尽。
思考题:
- 如果
innodb_flush_log_at_trx_commit = 2,MySQL 进程崩溃(不是操作系统崩溃)会丢数据吗?为什么? - 为什么崩溃恢复必须重做未提交的事务的修改,而不是跳过它们?
- 在你的错误日志中找到最近一次启动时的恢复记录,看看有没有
rollback of uncommitted transactions的信息。
参考资料
- MySQL 8.0 Reference Manual - InnoDB Crash Recovery
- MySQL 8.0 Reference Manual - InnoDB Redo Log
- MySQL 8.0 Reference Manual - InnoDB Checkpoints