Undolog是InnoDBMVCC事务特性的重要组成部分。当我们对记录做了变更操作时就会产生undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可以使用独立的Undo表空间。
Undo记录中存储的是老版本数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着undo链找到满足其可见性的记录。当版本链很长时,通常可以认为这是个比较耗时的操作
大多数对数据的变更操作包括INSERT/DELETE/UPDATE,其中INSERT操作在事务提交前只对当前事务可见,因此产生的Undo日志可以在事务提交后直接删除,而对于UPDATE/DELETE则需要维护多版本信息,在InnoDB里,UPDATE和DELETE操作产生的Undo日志被归成一类,即update_undo。
基本文件结构为了保证事务并发操作时,在写各自的undolog时不产生冲突,InnoDB采用回滚段的方式来维护undolog的并发写入和持久化。回滚段实际上是一种Undo文件组织方式,每个回滚段又有多个undologslot。具体的文件组织方式如下图所示:
上图展示了基本的Undo回滚段布局结构,其中:
rseg0预留在系统表空间ibdata中;
rseg1~rseg3这3个回滚段存放于临时表的系统表空间中;
rseg33~则根据配置存放到独立undo表空间中(如果没有打开独立Undo表空间,则存放于ibdata中)
如果我们使用独立Undotablespace,则总是从第一个Undospace开始轮询分配undo回滚段。大多数情况下这是OK的,但假设我们将回滚段的个数从33开始依次递增配置到18,就可能导致所有的回滚段都存放在同一个undospace中。
每个回滚段维护了一个段头页,在该page中又划分了个slot(TRX_RSEG_N_SLOTS),每个slot又对应到一个undolog对象,因此理论上InnoDB最多支持96*个普通事务。
关键结构体为了便于管理和使用undo记录,在内存中维持了如下关键结构体对象:
所有回滚段都记录在trx_sys-rseg_array,数组大小为18,分别对应不同的回滚段;
rseg_array数组类型为trx_rseg_t,用于维护回滚段相关信息;
每个回滚段对象trx_rseg_t还要管理undolog信息,对应结构体为trx_undo_t,使用多个链表来维护trx_undo_t信息;
事务开启时,会专门给他指定一个回滚段,以后该事务用到的undolog页,就从该回滚段上分配;
事务提交后,需要purge的回滚段会被放到purge队列上(purge_sys-purge_queue)。
各个结构体之间的联系如下:
分配回滚段当开启一个读写事务时(或者从只读事务转换为读写事务),我们需要预先为事务分配一个回滚段:
对于只读事务,如果产生对临时表的写入,则需要为其分配回滚段,使用临时表回滚段(第1~3号回滚段),函数入口:trx_assign_rseg--trx_assign_rseg_low--get_next_nodo_rseg。
在MySQL5.7中事务默认以只读事务开启,当随后判定为读写事务时,则转换成读写模式,并为其分配事务ID和回滚段,调用函数:trx_set_rw_mode--trx_assign_rseg_low--get_next_do_rseg。
普通回滚段的分配方式如下:
采用round-robin的轮询方式来赋予回滚段给事务,如果回滚段被标记为skip_allocation(这个undotablespace太大了,purge线程需要对其进行truncate操作),则跳到下一个;
选择一个回滚段给事务后,会将该回滚段的rseg-trx_f_count递增,这样该回滚段所在的undotablespace文件就不可以被truncate掉;
临时表回滚段被赋予trx-rsegs-m_nodo,普通读写操作的回滚段被赋予trx-rsegs-m_do;如果事务在只读阶段使用到临时表,随后转换成读写事务,那么会为该事务分配两个回滚段。
使用回滚段当产生数据变更时,我们需要使用Undolog记录下变更前的数据以维护多版本信息。insert和delete/update分开记录undo,因此需要从回滚段单独分配Undoslot。
入口函数:trx_undo_port_row_operation
流程如下:
判断当前变更的是否是临时表,如果是临时表,则采用临时表回滚段来分配,否则采用普通的回滚段;
临时表操作记录undo时不写dolog;
操作类型为TRX_UNDO_INSERT_OP,且未分配insertundoslot时,调用函数trx_undo_assign_undo进行分配;
操作类型为TRX_UNDO_MODIFY_OP,且未分配Updateundoslot时,调用函数trx_undo_assign_undo进行分配。
我们来看看函数trx_undo_assign_undo的流程:
首先总是从cahcedlist上分配trx_undo_t(函数trx_undo_use_cached,当满足某些条件时,事务提交时会将其拥有的trx_undo_t放到cachedlist上,这样新的事务可以重用这些undo对象,而无需去扫描回滚段,寻找可用的slot,在后面的事务提交一节会介绍到);
对于INSERT,从trx_rseg_t::insert_undo_cached上获取,并修改头部重用信息(trx_undo_insert_header_use)及预留XID空间(trx_undo_header_add_space_for_xid)
对于DELETE/UPDATE,从trx_rseg_t::update_undo_cached上获取,并在undologhdrpage上创建新的Undologheader(trx_undo_header_cate),及预留XID存储空间(trx_undo_header_add_space_for_xid)
获取到trx_undo_t对象后,会从cachedlist上移除掉。并初始化trx_undo_t相关信息(trx_undo_mem_init_for_use),将trx_undo_t::state设置为TRX_UNDO_ACTIVE
如果没有cache的trx_undo_t,则需要从回滚段上分配一个空闲的undoslot(trx_undo_cate),并创建对应的undo页,进行初始化;
一个回滚段可以支持个事务并发,如果不幸回滚段都用完了(通常这几乎不会发生),会返回错误DB_TOO_MANY_CONCURRENT_TRXS
每一个Undologsegment实际上对应一个独立的段,段头的起始位置在UNDO头page的TRX_UNDO_SEG_HDR+TRX_UNDO_FSEG_HEADER偏移位置(见下图)
已分配给事务的trx_undo_t会加入到链表trx_rseg_t::insert_undo_list或者trx_rseg_t::update_undo_list上;
如果是数据词典操作(DDL)产生的undo,主要是表级别操作,例如创建或删除表,还需要记录操作的tableid到undologheader中(TRX_UNDO_TABLE_ID),同时将TRX_UNDO_DICT_TRANS设置为TRUE。(trx_undo_mark_as_dict_operation)。
总的来说,undoheaderpage主要包括如下信息:
如何写入undo日志入口函数:trx_undo_port_row_operation
当分配了一个undoslot,同时初始化完可用的空闲区域后,就可以向其中写入undo记录了。写入的pageno取自undo-last_page_no,初始情况下和hdr_page_no相同。
对于INSERT_UNDO,调用函数trx_undo_page_port_insert进行插入,记录格式大致如下图所示:
对于UPDATE_UNDO,调用函数trx_undo_page_port_modify进行插入,UPDATEUNDO的记录格式大概如下图
在写入的过程中,可能出现单页面空间不足的情况,导致写入失败,我们需要将刚刚写入的区域清空重置(trx_undo_erase_page_end),同时申请一个新的page(trx_undo_add_page)加入到undolog段上,同时将undo-last_page_no指向新分配的page,然后重试。
完成Undolog写入后,构建新的回滚段指针并返回(trx_undo_build_roll_ptr),回滚段指针包括undolog所在的回滚段id、日志所在的pageno、以及page内的偏移量,需要记录到聚集索引记录中。
事务Ppa阶段入口函数:trx_ppa_low
当事务完成需要提交时,为了和BINLOG做XA,InnoDB的