MySQL 数据库在运行过程中可能因意外断电、进程崩溃等原因突然终止,此时未完成的事务、未刷盘的脏页等状态需要在下次启动时修复,这一过程即为崩溃恢复。本文基于 MySQL 8.0.29 版本,深入解析 InnoDB 存储引擎的崩溃恢复机制,涵盖数据页修复、Redo 日志应用、事务处理等核心环节,揭示 MySQL 如何通过多层保障机制确保数据一致性。
一、概述:崩溃恢复的核心目标
MySQL 正常关闭时会执行一系列收尾工作(如清理 undo 日志、合并 change buffer),而崩溃属于 "非正常终止",未完成的操作需在下次启动时处理。崩溃恢复的核心目标是:
- 利用 Redo 日志将未刷盘的脏页恢复到崩溃前的状态;
- 处理未完成的事务(提交、回滚或清理),确保数据逻辑一致。
该过程涉及 InnoDB 存储引擎的双重写(double write)、Redo 日志、undo 表空间等关键组件,协同完成数据修复与事务整理。
二、双重写:数据页完整性的第一道防线
InnoDB 的 Redo 日志能恢复脏页,但前提是数据页本身完好。若脏页刷盘过程中崩溃,数据页可能损坏(如只写入部分内容),此时需依赖双重写(double write) 机制修复。
1. 双重写的组成与工作流程
- 组成:包含内存缓冲区(double write buffer)和磁盘文件(dblwr 文件,默认位于数据目录,如
#ib_16384_0.dblwr
)。 - 工作流程:
当脏页需要刷盘时,InnoDB 先将脏页内容写入双重写内存缓冲区,再同步到 dblwr 文件;确认写入成功后,才将脏页真正刷入表空间(系统表空间、独立表空间等)。
2. 崩溃后的页修复逻辑
崩溃恢复时,InnoDB 会先加载 dblwr 文件中的所有页到内存缓冲区,再逐页检查并修复表空间中的损坏数据页:
- 判断损坏:通过数据页头部的
FILE_PAGE_LSN
(页的日志序列号)和尾部的校验和(checksum)判断:- 若
FILE_PAGE_LSN
大于当前 Redo 日志的最大 LSN,说明页损坏; - 若头部校验和与尾部校验和不匹配,说明页损坏。
- 若
- 修复过程:将双重写缓冲区中对应的完整页内容复制到损坏的表空间页,使其恢复到崩溃前最后一次正确刷盘的状态。
三、Redo 日志应用:脏页恢复的核心环节
修复数据页后,需通过 Redo 日志恢复未刷盘的脏页。这一过程需确定日志起点、读取日志并应用到对应页。
1. 确定 Redo 日志的起点:last_checkpoint_lsn
Redo 日志的应用需从最后一次检查点(checkpoint) 开始,对应的日志序列号为last_checkpoint_lsn
:
- 存储位置:位于第一个 Redo 日志文件的第 2、4 个 block(每个 block 512 字节),Checkpoint 信息交替写入这两个 block(奇数 checkpoint 号写入第 4 个,偶数写入第 2 个)。
- 选择逻辑:取两个 block 中较大的
last_checkpoint_lsn
作为起点(即使一个 block 损坏,另一个必为有效历史值)。
2. 读取 Redo 日志:解析与暂存
InnoDB 按以下流程读取 Redo 日志并预处理:
- 批量读取:以 64K(4 倍页大小)为单位从 Redo 日志文件读入
log buffer
,校验 block 完整性(通过头部和尾部信息)。 - 解析与暂存:将 block 中的有效日志(去除 12 字节头部和 4 字节尾部后的 496 字节)拷贝到 2M 的
parsing buffer
,解析出日志类型、表空间 ID、页号及数据,存入嵌套哈希表:- 第一层 key 为表空间 ID,value 为第二层哈希表;
- 第二层 key 为页号,value 为该页的 Redo 日志链表(按生成顺序排列)。
- 内存控制:哈希表借用 buffer pool 的内存,当占用空间超过阈值(buffer pool 剩余内存)时,先应用部分日志再继续读取,避免内存溢出。
3. 应用 Redo 日志到数据页
将哈希表中的日志批量应用到对应数据页,流程如下:
- 遍历哈希表:按表空间 ID 和页号遍历,获取每个页的 Redo 日志链表。
- 批量加载数据页:若页不在 buffer pool 中,计算页号范围(如
page_no
所在的 32 页区间),异步批量加载到 buffer pool(预读优化)。 - 应用日志:对比页的
FILE_PAGE_LSN
与日志的start_lsn
,仅应用start_lsn >= FILE_PAGE_LSN
的日志(过滤已刷盘的操作),直接修改 buffer pool 中的数据页。
四、undo 表空间处理:清理与重建
undo 表空间存储事务回滚所需的日志,崩溃可能导致截断操作未完成,需在恢复时处理。
1. 删除未完成截断的 undo 表空间
InnoDB 通过trunc.log
文件标记 undo 表空间的截断状态(如undo_1_trunc.log
对应 undo_001 表空间):
- 若文件内容为魔数
76845412
,说明截断已完成,直接删除该文件; - 若内容非魔数,说明截断未完成,删除对应的 undo 表空间文件(后续重建)。
2. 重建 undo 表空间
对需重建的 undo 表空间,按以下步骤操作:
- 删除旧
trunc.log
,创建新标记文件; - 新建 16M 的 undo 表空间文件,初始化表空间 ID 和链表信息;
- 创建回滚段(数量由
innodb_rollback_segments
控制),将回滚段中的 1024 个 undo slot 初始化为FIL_NULL
(未关联 undo 段); - 写入魔数
76845412
到trunc.log
,完成后删除该文件,标记重建完成。
五、事务处理:提交、回滚与清理
恢复数据页后,需处理崩溃时未完成的事务,确保逻辑一致性。事务状态分为三类,处理方式不同:
1. 清理已提交事务(TRX_STATE_COMMITTED_IN_MEMORY)
这类事务已完成二阶段提交,仅需收尾:
- 缓存或释放 insert undo 段(可复用的加入
insert_undo_cached
链表); - 从读写事务链表中移除,状态改为
TRX_STATE_NOT_STARTED
。
2. 回滚未提交事务(TRX_STATE_ACTIVE)
包括未提交的 DDL 和 DML 事务:
- DDL 事务:尽管用户无法回滚 DDL,但 MySQL 内部可通过 undo 日志回滚未完成的 DDL 操作;
- DML 事务:通过 undo 日志反向执行操作,撤销已修改的数据。
3. 处理 PREPARE 事务(TRX_STATE_PREPARED)
这类事务处于二阶段提交的 PREPARE 阶段,需结合 binlog 判断:
- 扫描最后一个 binlog 文件,收集所有
XID_EVENT
(事务 ID 记录); - 若事务 XID 在
XID_EVENT
集合中,说明 binlog 已记录,提交事务; - 若不在集合中,说明 binlog 未记录,回滚事务(保证主从一致性)。
六、总结:崩溃恢复的核心逻辑
MySQL 崩溃恢复通过多层机制保障数据一致性:
- 双重写修复损坏的数据页,为 Redo 日志应用奠定基础;
- Redo 日志恢复未刷盘的脏页,重现崩溃前的页状态;
- 事务处理通过 undo 日志和 binlog 协同,清理已完成事务、回滚未提交事务、根据 binlog 状态处理 PREPARE 事务。
这一过程既依赖 InnoDB 的物理恢复(数据页),也包含逻辑恢复(事务),最终确保数据库从崩溃状态安全恢复至一致状态。