背景

MySQL 版本: 8.0.25

数据库系统中关于事务有 4 个重要特性 ACID, 其中 A 代表的原子性: 一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性. 对于 InnoDB 来说, 针对意外崩溃情况,也需要保证事务满足原子性,即在崩溃前提交的事务需要保证重启后可读, 尚未提交的事务需要正确的回滚.

Redo Log

关于 Redo Log 在之前的文章 InnoDB 的 Redo Log 分析 已经详细介绍过, InnoDB 利用 Redo Log 来记录所有的数据和其他的文件操作. InnoDB 在对应操作的 Redo Log 落盘后就会给用户返回操作成功, 此时对应的数据 Page 可能还在 Buffer Pool 中尚未落盘, 这里可以加快的写入的速度, 但也需要在意外崩溃后能使数据库的数据 Page 恢复到一个正确的状态.

Undo Log

InnoDB 使用 MVCC + Undo Log 来实现不同的事务隔离级别, 在数据库正常的运行时,用户可以通过 Undo Log 来在不同的隔离级别下读取相应正确的数据, 其中在意外崩溃后,InnoDB 需要使用 Undo Log 来回滚尚未提交的事务.

Checkpoint

在 MySQL 8.0 新建了一个独立的线程log_checkpointer来执行 checkpoint 任务, 当 InnoDB 执行一次 checkpoint 时, 会将指定 lsn 位置的数据 Page 刷入磁盘, 这就保证了在此 lsn 之前的数据均以持久化. log_checkpointer在执行 checkpoint 后会写入 checkpoint 信息至ib_logfile0, InnoDB 设计在 offset 512 bytes 和 1536 bytes 轮流写 checkpoint 信息,防止某次写入 checkpoint 失败导致故障恢复无法找到上次的位点.

回滚流程

当 MySQL 启动后,无论之前是否发生 crash 都会尝试进行 recover (recv_recovery_from_checkpoint_start()):

  • 读取 checkpoint 信息,找到记录的最新的 checkpoint (recv_find_max_checkpoint()).

  • 将 checkpoint 之前的 Redo Log 重新进行 apply, 保证数据 Page 的正确性 (recv_apply_hashed_log_recs()).

  • InnoDB 针对 Undo Tablespace 的回滚段进行事务的重建(trx_sys_init_at_db_start() --> trx_rsegs_init()).

  • 重建回滚段后恢复当前事务列表(trx_lists_init_at_db_start()). (事务信息记录在回滚段中的 undo log segment, InnoDB 可以借此恢复事务信息).

  • 恢复 table id, 用以在数据字段恢复时重新加锁(srv_dict_recover_on_restart()).

事务恢复的回滚

  • 针对事务中存在 DDL 的操作, 采用同步回滚的方式innobase_dict_recover() --> srv_dict_recover_on_restart().

  • 针对不涉及数据字典操作的普通事务, InnoDB 采用异步事务回滚的方式, 通过新启一个线程trx_recovery_rollback_thread来回滚恢复出来的事务.

总结

事务的故障恢复重要的一个关键点是如何恢复意外 crash 前的事务状态信息, InnoDB 使用的 Undo Log 结构里为每个事务都会分配的 Undo Log Segment 持久化记录了事务的状态信息, 即使 Undo Page 尚未刷盘,也可以通过 Redo Log 也可以恢复了 Undo Page, Redo Log + Undo Log 保证了 InnoDB 关于事务实现的可靠性.