崩溃恢复揭秘:从 Redo Log 到数据一致性

在上一篇文章中,我们认识了 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 开始恢复,并扫描日志,找出所有包含"未完成操作"的页。

具体步骤:

  1. 从 Checkpoint 信息中找到最后一次 Checkpoint 的 LSN(checkpoint_lsn)。
  2. 从该 LSN 开始,顺序扫描 Redo Log 文件,直到末尾。
  3. 解析每条日志,记录下被修改的页号操作类型。这些信息会被存入一个哈希表(脏页表)。

这一步只是为了收集信息,不会修改任何数据页。扫描完毕后,InnoDB 就知道:

  • 有哪些页在崩溃时可能处于"不完整"状态。
  • 哪些事务在崩溃时还是活跃的(未提交)。

2.2 阶段二:重做(Redo)

目标 :将 Redo Log 中记录的所有修改重新应用到数据页上,无论事务是否提交

具体步骤:

  1. 再次从 checkpoint_lsn 开始扫描 Redo Log,一直扫到日志末尾。
  2. 对于每条日志,检查其对应的数据页的 FIL_PAGE_LSN
    • 如果 页 LSN < 日志 LSN:说明这次修改尚未写入磁盘,需要从磁盘读取该页,应用 Redo Log 中的修改,再将页写回磁盘。
    • 如果 页 LSN >= 日志 LSN:说明页已经包含了这次修改(可能在崩溃前刚好刷盘了),跳过。
  3. 全部日志扫描完成后,所有已提交和未提交的修改都已被反映到磁盘的数据页中。

为什么连未提交的修改也要重做?

因为未提交的事务可能在崩溃前已经把部分脏页刷到了磁盘(由后台刷盘线程所为),如果只重做已提交的日志,就会导致磁盘上的数据页出现"半成品"状态。所以必须全部重做,让所有页都达到崩溃瞬间的最新状态,之后再统一回滚未提交的事务。

2.3 阶段三:回滚(Undo)

目标:找出崩溃时所有未提交的事务,利用 Undo Log 回滚它们。

具体步骤:

  1. 扫描 Undo Log(活跃事务表),找出所有在崩溃时处于 ACTIVEPREPARED 状态的事务。
  2. 对于每个未提交的事务,沿着它的 Undo 版本链,逆向执行 Undo Log 中记录的操作(INSERT → DELETE,UPDATE → 恢复旧值)。
  3. 回滚完成后,这些事务的修改被彻底抹除,数据恢复到一致状态。

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 中恢复该页的完整副本。具体流程:

  1. 读取数据页时,计算校验和,与页尾的校验值比对。
  2. 如果不匹配,说明发生了页断裂(写入时断电)。
  3. InnoDB 查找 Doublewrite Buffer(系统表空间中连续 128 页),看是否有该页的完整副本。
  4. 若有,用副本覆盖损坏页,再继续应用 Redo Log。
  5. 若没有(极少情况),恢复失败,可能需要从备份恢复。

因此,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 ------ 扫描到的最大 LSN
  • apply 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 文件,将前面六篇文章的理论知识一网打尽。

思考题

  1. 如果 innodb_flush_log_at_trx_commit = 2,MySQL 进程崩溃(不是操作系统崩溃)会丢数据吗?为什么?
  2. 为什么崩溃恢复必须重做未提交的事务的修改,而不是跳过它们?
  3. 在你的错误日志中找到最近一次启动时的恢复记录,看看有没有 rollback of uncommitted transactions 的信息。

参考资料


相关推荐
AOwhisky32 分钟前
Redis 学习笔记(第一期):概述、安装配置与核心理论
运维·数据库·redis·笔记·学习·云计算
ytttr87339 分钟前
C# 定时数据库备份工具
开发语言·数据库·c#
睡不醒男孩0308231 小时前
自建 Prometheus+Grafana 与 CLUP 深度监控 PG 集群有什么区别?
数据库·oracle
AOwhisky1 小时前
Redis 学习笔记(第四期):高可用与集群(哨兵 + Cluster + 容器化)
linux·运维·数据库·redis·笔记·学习·缓存
猫猫聚会Ing1 小时前
数据库设计 Prompt 提示词 - 构建与迭代
数据库
上海云盾-小余1 小时前
源站隐藏实战:规避裸 IP 被直接攻击的完整方案
数据库·网络协议·tcp/ip
微学AI2 小时前
时序大模型 TimechoAI 赋能工业时序数据底层技术优势与实操
数据库·大模型·时序大模型
北顾笙9802 小时前
MYSQL-day03
数据库·sql·mysql
MXsoft6182 小时前
**混合云统一监控实践:私有云+公有云的一体化运维方案**
运维·网络·数据库
瀚高PG实验室3 小时前
java中间件无法连接数据库
java·数据库·中间件·瀚高数据库