MySQL · 引擎特性 · InnoDB undo log 漫游

KevinMitnick 2016-03-02

本文是对整个Undo生命周期过程的阐述,代码分析基于当前最新的MySQL5.7版本。本文也可以作为了解整个Undo模块的代码导读。由于涉及到的模块众多,因此部分细节并未深入。

前言

Undo log是InnoDB MVCC事务特性的重要组成部分。当我们对记录做了变更操作时就会产生undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可以使用独立的Undo 表空间。

Undo记录中存储的是老版本数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着undo链找到满足其可见性的记录。当版本链很长时,通常可以认为这是个比较耗时的操作(例如bug#69812)。

大多数对数据的变更操作包括INSERT/DELETE/UPDATE,其中INSERT操作在事务提交前只对当前事务可见,因此产生的Undo日志可以在事务提交后直接删除(谁会对刚插入的数据有可见性需求呢!!),而对于UPDATE/DELETE则需要维护多版本信息,在InnoDB里,UPDATE和DELETE操作产生的Undo日志被归成一类,即update_undo。

基本文件结构

为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo log的并发写入和持久化。回滚段实际上是一种 Undo 文件组织方式,每个回滚段又有多个undo log slot。具体的文件组织方式如下图所示:

MySQL · 引擎特性 · InnoDB undo log 漫游

上图展示了基本的Undo回滚段布局结构,其中:

  1. rseg0预留在系统表空间ibdata中;
  2. rseg 1~rseg 32这32个回滚段存放于临时表的系统表空间中;
  3. rseg33~ 则根据配置存放到独立undo表空间中(如果没有打开独立Undo表空间,则存放于ibdata中)

如果我们使用独立Undo tablespace,则总是从第一个Undo space开始轮询分配undo 回滚段。大多数情况下这是OK的,但假设我们将回滚段的个数从33开始依次递增配置到128,就可能导致所有的回滚段都存放在同一个undo space中。(参考函数trx_sys_create_rsegs 以及 bug#74471)

每个回滚段维护了一个段头页,在该page中又划分了1024个slot(TRX_RSEG_N_SLOTS),每个slot又对应到一个undo log对象,因此理论上InnoDB最多支持 96 * 1024个普通事务。

关键结构体

为了便于管理和使用undo记录,在内存中维持了如下关键结构体对象:

  1. 所有回滚段都记录在trx_sys->rseg_array,数组大小为128,分别对应不同的回滚段;
  2. rseg_array数组类型为trx_rseg_t,用于维护回滚段相关信息;
  3. 每个回滚段对象trx_rseg_t还要管理undo log信息,对应结构体为trx_undo_t,使用多个链表来维护trx_undo_t信息;
  4. 事务开启时,会专门给他指定一个回滚段,以后该事务用到的undo log页,就从该回滚段上分配;
  5. 事务提交后,需要purge的回滚段会被放到purge队列上(purge_sys->purge_queue)。

各个结构体之间的联系如下:

MySQL · 引擎特性 · InnoDB undo log 漫游

分配回滚段

当开启一个读写事务时(或者从只读事务转换为读写事务),我们需要预先为事务分配一个回滚段:

对于只读事务,如果产生对临时表的写入,则需要为其分配回滚段,使用临时表回滚段(第1~32号回滚段),函数入口:trx_assign_rseg -->trx_assign_rseg_low-->get_next_noredo_rseg

在MySQL5.7中事务默认以只读事务开启,当随后判定为读写事务时,则转换成读写模式,并为其分配事务ID和回滚段,调用函数:trx_set_rw_mode -->trx_assign_rseg_low --> get_next_redo_rseg

普通回滚段的分配方式如下:

  1. 采用round-robin的轮询方式来赋予回滚段给事务,如果回滚段被标记为skip_allocation(这个undo tablespace太大了,purge线程需要对其进行truncate操作),则跳到下一个;
  2. 选择一个回滚段给事务后,会将该回滚段的rseg->trx_ref_count递增,这样该回滚段所在的undo tablespace文件就不可以被truncate掉;
  3. 临时表回滚段被赋予trx->rsegs->m_noredo,普通读写操作的回滚段被赋予trx->rsegs->m_redo;如果事务在只读阶段使用到临时表,随后转换成读写事务,那么会为该事务分配两个回滚段。

使用回滚段

当产生数据变更时,我们需要使用Undo log记录下变更前的数据以维护多版本信息。insert 和 delete/update 分开记录undo,因此需要从回滚段单独分配Undo slot。

入口函数:trx_undo_report_row_operation

流程如下:

  1. 判断当前变更的是否是临时表,如果是临时表,则采用临时表回滚段来分配,否则采用普通的回滚段;
  2. 临时表操作记录undo时不写redo log;
  3. 操作类型为TRX_UNDO_INSERT_OP,且未分配insert undo slot时,调用函数trx_undo_assign_undo进行分配;
  4. 操作类型为TRX_UNDO_MODIFY_OP,且未分配Update undo slot时,调用函数trx_undo_assign_undo进行分配。

我们来看看函数trx_undo_assign_undo的流程:

  1. 首先总是从cahced list上分配trx_undo_t (函数trx_undo_reuse_cached,当满足某些条件时,事务提交时会将其拥有的trx_undo_t放到cached list上,这样新的事务可以重用这些undo 对象,而无需去扫描回滚段,寻找可用的slot,在后面的事务提交一节会介绍到);
    • 对于INSERT,从trx_rseg_t::insert_undo_cached上获取,并修改头部重用信息(trx_undo_insert_header_reuse)及预留XID空间(trx_undo_header_add_space_for_xid)
    • 对于DELETE/UPDATE,从trx_rseg_t::update_undo_cached上获取, 并在undo log hdr page上创建新的Undo log header(trx_undo_header_create),及预留XID存储空间(trx_undo_header_add_space_for_xid)
    • 获取到trx_undo_t对象后,会从cached list上移除掉。并初始化trx_undo_t相关信息(trx_undo_mem_init_for_reuse),将trx_undo_t::state设置为TRX_UNDO_ACTIVE
  2. 如果没有cache的trx_undo_t,则需要从回滚段上分配一个空闲的undo slot(trx_undo_create),并创建对应的undo页,进行初始化;

    一个回滚段可以支持1024个事务并发,如果不幸回滚段都用完了(通常这几乎不会发生),会返回错误DB_TOO_MANY_CONCURRENT_TRXS

    每一个Undo log segment实际上对应一个独立的段,段头的起始位置在UNDO 头page的TRX_UNDO_SEG_HDR+TRX_UNDO_FSEG_HEADER偏移位置(见下图)

  3. 已分配给事务的trx_undo_t会加入到链表trx_rseg_t::insert_undo_list或者trx_rseg_t::update_undo_list上

  4. 如果是数据词典操作(DDL)产生的undo,主要是表级别操作,例如创建或删除表,还需要记录操作的table id到undo log header中(TRX_UNDO_TABLE_ID),同时将TRX_UNDO_DICT_TRANS设置为TRUE。(trx_undo_mark_as_dict_operation)。

总的来说,undo header page主要包括如下信息:
MySQL · 引擎特性 · InnoDB undo log 漫游

如何写入undo日志

入口函数:trx_undo_report_row_operation

当分配了一个undo slot,同时初始化完可用的空闲区域后,就可以向其中写入undo记录了。写入的page no取自undo->last_page_no,初始情况下和hdr_page_no相同。

对于INSERT_UNDO,调用函数trx_undo_page_report_insert进行插入,记录格式大致如下图所示:
MySQL · 引擎特性 · InnoDB undo log 漫游

对于UPDATE_UNDO,调用函数trx_undo_page_report_modify进行插入,UPDATE UNDO的记录格式大概如下图所示:
MySQL · 引擎特性 · InnoDB undo log 漫游

在写入的过程中,可能出现单页面空间不足的情况,导致写入失败,我们需要将刚刚写入的区域清空重置(trx_undo_erase_page_end),同时申请一个新的page(trx_undo_add_page) 加入到undo log段上,同时将undo->last_page_no指向新分配的page,然后重试。

完成Undo log写入后,构建新的回滚段指针并返回(trx_undo_build_roll_ptr),回滚段指针包括undo log所在的回滚段id、日志所在的page no、以及page内的偏移量,需要记录到聚集索引记录中。

相关推荐