MySQL 崩溃恢复, Redo 日志修复

发布于:2025-07-28 ⋅ 阅读:(13) ⋅ 点赞:(0)

MySQL 数据库在运行过程中可能因意外断电、进程崩溃等原因突然终止,此时未完成的事务、未刷盘的脏页等状态需要在下次启动时修复,这一过程即为崩溃恢复。本文基于 MySQL 8.0.29 版本,深入解析 InnoDB 存储引擎的崩溃恢复机制,涵盖数据页修复、Redo 日志应用、事务处理等核心环节,揭示 MySQL 如何通过多层保障机制确保数据一致性。

一、概述:崩溃恢复的核心目标

MySQL 正常关闭时会执行一系列收尾工作(如清理 undo 日志、合并 change buffer),而崩溃属于 "非正常终止",未完成的操作需在下次启动时处理。崩溃恢复的核心目标是:

  1. 利用 Redo 日志将未刷盘的脏页恢复到崩溃前的状态;
  2. 处理未完成的事务(提交、回滚或清理),确保数据逻辑一致。

该过程涉及 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 日志并预处理:

  1. 批量读取:以 64K(4 倍页大小)为单位从 Redo 日志文件读入log buffer,校验 block 完整性(通过头部和尾部信息)。
  2. 解析与暂存:将 block 中的有效日志(去除 12 字节头部和 4 字节尾部后的 496 字节)拷贝到 2M 的parsing buffer,解析出日志类型、表空间 ID、页号及数据,存入嵌套哈希表:
    • 第一层 key 为表空间 ID,value 为第二层哈希表;
    • 第二层 key 为页号,value 为该页的 Redo 日志链表(按生成顺序排列)。
  3. 内存控制:哈希表借用 buffer pool 的内存,当占用空间超过阈值(buffer pool 剩余内存)时,先应用部分日志再继续读取,避免内存溢出。

3. 应用 Redo 日志到数据页

将哈希表中的日志批量应用到对应数据页,流程如下:

  1. 遍历哈希表:按表空间 ID 和页号遍历,获取每个页的 Redo 日志链表。
  2. 批量加载数据页:若页不在 buffer pool 中,计算页号范围(如page_no所在的 32 页区间),异步批量加载到 buffer pool(预读优化)。
  3. 应用日志:对比页的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 表空间,按以下步骤操作:

  1. 删除旧trunc.log,创建新标记文件;
  2. 新建 16M 的 undo 表空间文件,初始化表空间 ID 和链表信息;
  3. 创建回滚段(数量由innodb_rollback_segments控制),将回滚段中的 1024 个 undo slot 初始化为FIL_NULL(未关联 undo 段);
  4. 写入魔数76845412trunc.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 崩溃恢复通过多层机制保障数据一致性:

  1. 双重写修复损坏的数据页,为 Redo 日志应用奠定基础;
  2. Redo 日志恢复未刷盘的脏页,重现崩溃前的页状态;
  3. 事务处理通过 undo 日志和 binlog 协同,清理已完成事务、回滚未提交事务、根据 binlog 状态处理 PREPARE 事务。

这一过程既依赖 InnoDB 的物理恢复(数据页),也包含逻辑恢复(事务),最终确保数据库从崩溃状态安全恢复至一致状态。


网站公告

今日签到

点亮在社区的每一天
去签到