InnoDB 的事务故障恢复流程
背景
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 来回滚尚未提交的事务.
启动流程
1 | mysqld_main() -> init_server_components() -> |
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()
).针对不完整的 mtr 的 redo log 情况下:
- 会 apply 到完整的 mtr redo log, 然后丢弃不完整的 mtr redo log.
- 将最后一个完整的 mtr redo log 的最后一个 block 内容拷贝至 log_sys->buf, 目的是进行 log_start().
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 关于事务实现的可靠性.