【华为云MySQL技术专栏】MySQL8.0 InnoDB崩溃恢复流程解析

举报
GaussDB 数据库 发表于 2025/06/09 14:54:31 2025/06/09
【摘要】 1、背景介绍数据库系统与文件系统的核心差异,在于数据库系统能够最大限度地保证ACID特性。在ACID特性中,数据一致性尤为重要。在崩溃恢复场景下,InnoDB引擎是通过Redo Log(重做日志,记录数据页的物理修改)和Undo Log(撤销日志,记录事务中更新前的历史数据)协同来实现数据一致性这一目标的。当数据库异常崩溃后重启,会先触发Roll-forward(前滚),通过重放Redo L...

MySQL顶部banner.jpg

1、背景介绍

数据库系统与文件系统的核心差异,在于数据库系统能够最大限度地保证ACID特性。在ACID特性中,数据一致性尤为重要。

在崩溃恢复场景下,InnoDB引擎是通过Redo Log(重做日志,记录数据页的物理修改)和Undo Log(撤销日志,记录事务中更新前的历史数据)协同来实现数据一致性这一目标的。

当数据库异常崩溃后重启,会先触发Roll-forward(前滚),通过重放Redo Log中所有事务的物理修改,实现数据持久性。继而触发Roll-back(回滚),基于Undo Log回滚未完成的事务,同时清理崩溃时未完成事务的中间状态和临时资源。最终,将数据库恢复到崩溃前的一致性状态,确保所有已提交事务的修改已持久化,未提交事务的修改不生效

本文将为大家详述InnoDB在崩溃恢复场景下维护数据一致性的原理。

2、Redo Log实现数据库前滚

数据库前滚恢复,主要依赖Redo Log体系。

InnoDB会首先找到最后的Checkpoint LSN,以此LSN为起点,逐条扫描后续的Redo Log。每条Redo Log均包含以下关键信息:表空间ID(space id)、页号(page no)以及物理修改记录。基于space id和page no,InnnoDB将Redo Log分发到哈希表hash_table中,保证同一个数据页的Redo Log被分发到同一个哈希桶中。扫描完后,再遍历整个哈希表,依次应用每个数据页的Redo Log,保证每个数据页将会恢复到崩溃之前的状态。

下面详细分析一下实现原理。

MySQL在启动时会默认创建两个文件:ib_logfile0和ib_logfile1,这两个文件以循环写入的方式交替记录Redo Log,并通过双Checkpoint保证写入的安全性——InnoDB在ib_logfile0的头部预留了两个Checkpoint区域,这两个字段交替更新,确保即使一个Checkpoint区域损坏,另一个仍可用。

首先,InnoDB会从ib_logfile0头中读取最新的Checkpoint LSN。Checkpoint LSN之前数据页的修改都已经落盘,不需要前滚,之后的数据页的修改可能还没落盘,需要根据Redo Log重新恢复出来。
Redo Log记录的是数据页的物理修改,每个修改对应一个LSN标识,通过LSN标识可以判断Redo Log是否需要重做,这种设计使得Redo Log可重复应用,所以即便数据页已经部分落盘了,重复应用Redo Log也不会影响数据正确性,下文将详细说明。

然后,InnoDB会以RECV_SCAN_SIZE(64Kb)大小为单位,从ib_logfile日志文件中指定位置(最新的Checkpoint LSN)开始读取Redo Log Block到日志缓冲区Log Buffer中。
对于已经读取到Log Buffer中的Redo Log Block,会利用Header和Tailer中的信息对Block进行完整性检验,并把具体的Redo Log数据(Block Body)拷贝到解析缓冲区Parsing Buffer中。Parsing Buffer是一个2M的固定大小缓冲区,用于存放即将要被解析的Redo Log。

接下来,调用recv_parse_log_recs函数对Parsing Buffer中的日志数据进行解析,然后放到前面提到的hash_table中。这一步解析Redo Log,实际上只是个预处理操作,并不会完整的解析Redo Log,只会解析每一条Redo Log中的头信息以及数据地址,包括4个部分:Redo类型、表空间ID、页号和Redo Log数据在Block Body中的地址recv_addr。
recv_addr通过recv_t结构将属于同一数据页的Redo Log Record串在一起,每个recv_t对应于一条Redo Log Record,并记录该record的type、len、data(日志内容)、start lsn、end lsn。同一个page的recv_t按照lsn顺序串在一起。
存放Redo Log的hash_table是个嵌套结构,同一个表空间的Redo Log以页单位组织到一起,存放到以表空间ID为key的第1层hash value中,同一个数据页的Redo Log链表以页号为key,放在第2层hash value中。链表中的Redo Log按照产生的先后顺序排列,第1条就是要应用的这些Redo Log中最早产生的那条。当hash表中存放的数据recv_addr达到一定的大小之后,就会调用recv_apply_hashed_log_recs函数应用Redo Log,应用完成之后,清空 hash_table,为下一批 Redo Log数据腾出空间。
应用Redo Log就是循环遍历这个嵌套的哈希表,把每一条Redo Log都按照space_id和page_no应用到对应的数据页中,具体会从数据页头部Page Header中读取最后一次修改该页的日志记录的LSN(FILE_PAGE_LSN),循环Redo Log链表中的每一条日志,判断该日志的start_lsn是否大于等于FILE_PAGE_LSN。
如果start_lsn < FILE_PAGE_LSN,说明该Redo Log对应的修改记录,在MySQL崩溃之前就已经刷盘,实现持久化,该Redo Log不再需要应用到数据页。
如果start_lsn >= FILE_PAGE_LSN,说明该Redo Log需要应用到数据页。然后,根据Redo Log类型,调用不同的方法解析Redo Log,直接修改Buffer Pool中的数据页,对该数据页应用Redo Log就完成了。

以下是数据库前滚恢复过程中各数据结构的关系和流程图:

1.png

图1 前滚恢复中的数据结构关系图

执行完Roll-forward,整个InnoDB Recovery的第一阶段也就结束了。在该阶段中,所有的Redo Log中记录的修改都被应用到了内存的数据页上,保证了内存数据页与Redo Log中的修改记录一致,但并不会立即将页面刷盘,刷盘将由后台线程异步完成。 

3、Undo Log实现数据库回滚

但仅仅通过前滚恢复是不够的,数据库崩溃的时候可能有一些没有提交的事务或者提交一半的事务,这个时候就需要重建崩溃前的事务,并依据事务的不同状态,进行回滚或者提交。这主要分为三步:

第一步:扫描Undo表空间,重新建立起回滚段的内存结构trx_rseg_t。
第二步:依据上一步建立起的trx_rseg_t,重建崩溃前的事务,即恢复当时事务的状态。
第三步,就是依据事务的不同状态,进行回滚或者提交。

实现的流程也比较简单,Undo Log本身的修改也会被记录到Redo Log中,这意味着,即使Undo Log的物理页未及时刷盘,崩溃后仍可通过Redo Log重建Undo信息。在Redo Log应用完成后,Undo Log页也会恢复到崩溃之前的状态。随后Innodb通过trx_sys_init_at_db_start初始化事务子系统,此过程的核心是回滚段相关内存结构以及事务链表的重建。

第一步
扫描所有的Undo表空间,并读取表空间中每个回滚段的物理页号,根据页号解析出对应回滚段的头部数据Rseg_header,该结构包含回滚段的元数据信息。在函数trx_rseg_mem_create中会根据Rseg_header创建trx_rseg_t回滚段的内存对象、insert_undo_list(插入类型的undo链表)、update_undo_list(更新类型的undo链表)等数据结构。

接着会遍历回滚段的所有Undo槽(Slot),将磁盘上的Undo Log段信息加载到内存中,并构建对应的trx_undo_t对象(用于管理单个事务的Undo log)。

Undo Log主要分为两种类型:TRX_UNDO_INSERTTRX_UNDO_UPDATE。前者主要是提供给insert操作用的,后者是给UPDATE和DELETE操作使用。

Undo Log有两种作用,事务回滚的时候用MVCC快照读取的时候用。由于INSERT的数据不需要提供给其他线程用,所以只要事务提交,就可以删除TRX_UNDO_INSERT类型的Undo Log。TRX_UNDO_UPDATE在事务提交后还不能删除,需要保证没有快照使用它的时候,才能通过后台的Purge线程清理。

不同类型的trx_undo_t对象会被添加到不同的Undo链表中,创建出来的Undo链表有4种类型:

rseg->insert_undo_listrseg->update_undo_list分别存储当前活跃事务中与 TRX_UNDO_INSERT类和TRX_UNDO_UPDATE类型的Undo Log。

rseg->insert_undo_cachedrseg->update_undo_cached分别存储已提交事务中可复用的TRX_UNDO_INSERT类型和TRX_UNDO_UPDATE类型相关的Undo Log。

回滚段内存结构初始化的关键代码如下:

2.png

第二步

由于第一步中已经在内存中建立起了undo_insert_list和undo_update_list链表,所以这一步只需要遍历这两个链表,根据trx_undo_t对象重建起崩溃前的事务状态,根据Undo Log的state字段判断事务状态:

如果Undo Log的状态是TRX_UNDO_ACTIVE,则事务的状态为TRX_ACTIVE
如果Undo Log的状态是TRX_UNDO_PREPARED,则事务的状态为TRX_PREPARED
既不是TRX_UNDO_ACTIVE,也不是TRX_UNDO_PREPARED状态的,则事务的状态将被标记为TRX_STATE_COMMITTED_IN_MEMORY

以上需要解析的事务状态包括:

TRX_STATE_ACTIVE(事务未提交处理活跃状态,需要强制回滚清理残余操作)
TRX_STATE_PREPARED(已提交一半的事务,需要通过XA机制恢复最终状态)
TRX_STATE_COMMITTED_IN_MEMORY(已提交状态,但资源未完全释放)

如果Undo Log的状态是TRX_UNDO_ACTIVE,则事务的状态为TRX_ACTIVE,重建起事务后,将按照事务id加入到trx_sys->trx_list链表中。接着会统计所有需要回滚的事务(事务状态为TRX_ACTIVE)一共需要回滚多少行数据,输出到错误日志中,类似:

5 transaction(s) which must be rolled back or cleaned up。InnoDB: in total 342232 row operations to undo的字样。

第三步

最后,会去开启一个后台线程来做事务回滚和清理,若Binlog开关关闭,事务提交会变成单阶段提交,因此无PREPARED事务需要协调,对于处于TRX_STATE_ACTIVE状态的事务直接执行回滚操作,对于处于TRX_STATE_COMMITTED_IN_MEMORY状态的事务,由于已经是内存已提交状态,所以只需要执行释放事务对象等资源回收的收尾操作。

若Binlog开关打开,MySQL会使用两阶段提交(2PC)来保证Binlog和InnoDB的Redo Log之间的一致性。Binlog打开场景,MySQL对于TRX_STATE_ACTIVE与TRX_STATE_COMMITTED_IN_MEMORY状态的事务与Binlog关闭场景的处理方式保持一致。完成上述状态的事务处理后,理论上事务链表trx_sys->trx_list上只存在PREPARE状态的事务。而这一部分需要和Server层的Binlog联合来进行崩溃恢复。回到Server层,在初始化完了InnoDB已经其他支持XA的存储引擎后,如果Binlog打开了,我们就可以通过Binlog来进行XA恢复:

1、首先扫描最后一个Binlog文件,找到其中所有的XID Event,并将其中的XID记录到info->commit_list哈希表中;注意因为在每次rotate到一个新的Binlog文件之前,总是要保证前一个Binlog文件中对应的事务都提交并且sync redo到磁盘了,也就是说,前一个Binlog文件中的事务一定不需要再进行XA恢复,系统只需要关注最后一个Binlog文件即可。
2、接着遍历所有支持XA的存储引擎,包括InnoDB引擎,每个引擎通过xarecover_handlerton接口批量返回其PREPARED状态的XID列表,其中InnoDB则通过扫描上文构建的事务链表trx_sys->trx_list获取PREPARED事务和对应XID。最后通过与info->commit_list哈希表做比较,拿到每个事务引擎中处于PREPARED状态的事务XID,如果这个XID存在于info->commit_list后,说明存在于Binlog中,则提交当前事务;否则回滚当前事务。

XA Recover阶段的关键流程如下:

3.png

 图2 XA Recover关键流程图

4、总结

MySQL通过Redo LogUndo Log的协同机制,在事务执行与崩溃恢复过程中来维护数据一致性:

Redo Log以物理日志形式记录数据页修改,确保未持久化的操作在崩溃后通过“前滚”恢复至崩溃前状态;已提交的事务将由Redo Log保证持久性和原子性,而未提交的事务才依赖Undo Log。事务中的每条数据变更(INSERT/DELETE/UPDATE)都会生成逆向操作的逻辑日志,记为Undo Log,Undo Log通过记录数据变更前的历史版本,确保未提交的事务可回滚到初始状态,实现“全做或者全不做”的原子性。

这两类日志在数据库常规操作中虽然对上层业务是“透明”的,但其底层协同是事务原子性、持久性的核心保障,同时为数据复制、备份及时间点恢复(PITR)提供了基础支持,最终实现在无需DBA干预下,若发生常规故障场景(进程异常终止、短暂断电等),数据库可自动恢复到崩溃前的一致性状态(满足ACID特性,已提交事务的所有修改持久化,未提交事务完全回滚,且不存在中间状态数据)。但需注意,在极端场景(如内存跳变导致错误数据写入或者数据损坏等),仅依赖日志可能依然无法完全恢复。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。