1. Undo Log的简介
在InnoDB的设计中,Undo Log主要参与了两件重要的事:崩溃恢复(Crash Recovery)和多版本并发控制(Multi-Version Concurrency Control, MVCC)。Undo Log记录了数据修改前的版本,Undo Log也像用户数据一样存储于表空间中,Undo Log也受redo Log提供的原子性保护。本文将从Undo Log的类型、Undo的存储、Undo Log的生成、Undo Log在崩溃恢复中的使用、Undo Log在多版本并发中的使用、Undo Log的清理等方面介绍Undo Log。
2. Undo Log的类型
Undo Log的类型总体而言,分为两大类:Insert Undo和Update Undo。
2.1 Insert Undo
Insert Undo是事务Insert记录时产生的Undo Log。Insert操作之所以要记录Undo,是方便在用户手动rollback或者事务运行中间系统崩溃,需要通过Crash Recovery删除记录时方便。严格来说,以某种方式标记新插入的记录,再通过全表扫描删除这些新插入,但也被回滚的记录也是可以的,但这样效率太低了。
Insert Undo大类下,只对应一种Insert格式:TRX_UNDO_INSERT_REC。格式如下:
名称 | 含义 |
---|---|
Next record offset | 在Undo Log页面中这条记录开始的地址 |
TRX_UNDO_INSERT_REC | Undo Log类型 |
Undo no | 一个事务内的递增编号 |
Table id | Undo Log对应的表在数据字典中的id |
主键各列 |
记录了主键各列的长度,已经对应的value。例如如果主键只有一个int列,其值为10。那么此处是<4, 10> |
Prev Record offset | 在Undo Log页面中这条记录结束的地址 |
需要注意的是:当插入一条记录到表中时,聚簇索引和二级索引都需要插入相应的信息。而Undo日志并不需要针对不同的页面记录两条,只需要在TRX_UNDO_INSERT_REC类型的Undo Log中记录完整的主键信息即可。在回滚或者Crash Recovery时需要彻底删除记录时,有了TRX_UNDO_INSERT_REC中完整的主键信息,即可删除聚簇索引和二级索引上的相应记录。从这个角度来说,Undo Log是逻辑日志,与作为对比的逻辑物理日志redo Log不同。redo Log的详细介绍见《InnoDB页面持久化》
2.2 Update Undo
除了Insert Undo外的所有Undo Log都属于Update Undo。详细来说包含一下几类:
2.2.1 TRX_UNDO_DEL_MARK_REC
DELETE一条记录可以分为三个阶段:
- 第一阶段:仅仅将记录的记录头中的delete_flag修改为true,做标记删除。记录格式相关内容详见《InnoDB行格式解析》。TRX_UNDO_DEL_MARK_REC类型就是对记录delete mark删除的Undo Log类型。
- 第二阶段:从记录被delete mark后,到此事务提交,再到其他事务也再也没有对这条记录的可见性需求前。这条delete mark的记录服务于MVCC。
- 第三阶段:当这条记录不再被任何事务需要时,其将被Purge线程彻底删除,也就是把记录加入每个索引页面的可重用PAGE_FREE链表,将此被删除记录作为PAGE_FREE头部;修改相应页面的用户记录数量PAGE_N_RECS;修改可回收的空间总大小PAGE_GARBAGE等信息。页面结构相关内容详见《InnoDB页面结构解析》。上述第二阶段和第三阶段在本文后续章节还会有详细介绍。
2.2.2 TRX_UNDO_UPD_EXIST_REC
InnoDB记录的更新分三类,下面分别介绍每类更新产生的Undo Log类型:
- 不更新主键,并且所有字段的长度不变化,那么更新将会在原地进行(in-place Update)。此类修改会记录TRX_UNDO_UPD_EXIST_REC类型的Undo Log。
- 不更新主键,但部分字段占用当空间有所变化,不论空间是变小或者变大。此类记录更新分为两步:第一步:先彻底删除旧记录。对旧记录的删除和Purge操作一样是彻底删除,不是delete mark。第二步:插入更新后的新记录,新记录继承被彻底删除的旧记录的Undo Log(roll_ptr与旧记录相同)。对于此类更新可以理解为:为待更新记录在相同页面中寻找一个更合适的位置,由于旧记录的Undo Log被继承了,该记录的所有旧版本都能通过新插入的记录找到,所以可以放心地删除旧记录。此类更新虽然在页面上的修改比较复杂,但是仍然只会记录TRX_UNDO_UPD_EXIST_REC类型的Undo Log。
- 更新主键。B+树的每个页面按照主键的大小逻辑排序。如果在原地更新记录,将可能导致页面乱序。因此此类更新的方式是先对旧记录进行delete mark,然后再插入新的记录。之所以这里不能彻底删除旧记录是因为,更新后的记录作为新记录插入,没有继承旧记录的Undo Log(roll_ptr指向Insert Undo),而旧记录在其他事物中可能有可见性需求,因此不能彻底删除旧记录,只能进行delete mark)。综上分析,此类更新会产生两条Undo Log:TRX_UNDO_DEL_MARK_REC + TRX_UNDO_INSERT_REC。
2.2.3 TRX_UNDO_UPD_DEL_REC
TRX_UNDO_UPD_DEL_REC从名称上看是对delete mark的记录的修改。但delete mark的记录一旦提交,就只能被Purge线程彻底删除,不能被其他事务修改。因此,TRX_UNDO_UPD_DEL_REC产生于一种比较特殊的情况:当记录被delete mark之后,同一个事务再次插入一条主键与被刚被delete mark的记录相同的记录,并且新旧记录的各字段占用空间相同时,即可直接复用被delete mark的记录的空间,只需要修改被delete mark的记录的部分列即可。
在一个事务中的TRX_UNDO_DEL_MARK_REC + TRX_UNDO_UPD_DEL_REC两个操作由于事务的原子性,要么都失败,要么都成功,因此二者合并在一起可以理解为是一次原地进行(in-place Update)。
2.2.4 Update Undo的格式
TRX_UNDO_DEL_MARK_REC、TRX_UNDO_UPD_EXIST_REC、TRX_UNDO_UPD_DEL_REC三类Update Undo总体比较相似,都包括table id、info bits、old trx_id、old roll_ptr、索引各列值等信息。但由于TRX_UNDO_DEL_MARK_REC不需要记录更新列的旧值,它是三类中最简单的。
下面以TRX_UNDO_UPD_EXIST_REC为例介绍Update Undo的格式:
名称 | 含义 |
---|---|
Next Record offset | 在Undo Log页面中这条记录开始的地址 |
TRX_UNDO_UPD_EXIST_REC | Undo Log类型 |
Undo no | 一个事务内的递增编号 |
Table id | Undo Log对应的表在数据字典中的id |
Info bits | 记录头中delete_flag、min_rec_flag(B+树非叶子结点中每一层最小的记录会添加此标识)、Record_type信息 |
Old trx_id | 旧记录的的事务ID |
Old roll_ptr | 旧记录的的回滚段指针 |
主键各列 |
主键各列的长度,已经对应的value。例如如果主键只有一个int列,其值为10。那么此处是<4, 10> |
n_Update | 被更新的列的个数 |
更新列旧值 |
更新列旧值列表 |
index_col_info len | 表示索引各列所占的空间 + index_col_info len本身占用的空间 |
索引各列 |
索引列表 |
Prev Record offset | 在Undo Log页面中这条记录结束的地址 |
- 更新列旧值
列表与索引各列 列表中的pos是列在记录中的位置,包含了row_id、trx_id、roll_ptr等隐藏列。如果用户记录中没有主键,那么row_id的pos为0,trx_id的pos为1,roll_ptr的pos为2,第一个用户列的pos为3。 - 索引各列
列表主要是为了辅助Purge操作清理二级索引而记录的。对于TRX_UNDO_DEL_MARK_REC而言,由于在Purge时需要清理二级索引上的记录。所以记录包含在二级索引中的所有列的信息。对于TRX_UNDO_UPD_EXIST_REC、TRX_UNDO_UPD_DEL_REC而言,只有更新的列包含二级索引的列时才记录。否则的话是不会添加这个部分的。
3. Undo Log的存储
3.1 Rollback segment
用户可以使用innodb_rollback_segments配置回滚段(Rollback segment)数量。InnoDB默认有128个回滚段,0号回滚段用于系统表,无论用户如何配置,都存在于系统表空间(ibdata)中。1-32号回滚段用于临时表空间,存储于ibtmp1中。33号-127号回滚段也用于普通表,用户可以通过innodb_undo_tablespaces设置独立Undo表空间的数量,将33号-127号回滚段均匀地分布在Undo独立表空间中,将回滚段设置到独立的Undo表空间中的优势在于在Undo表空间中的文件大到一定程度时,可以将该Undo表空间截断(truncate)成一个小文件。而系统表空间存放了系统表等数据,其大小只能不断的增大,却不能截断。如果innodb_undo_tablespaces是默认值0,那么33号-127号回滚段也将存在于系统表空间中。
系统表空间的第六个页面(page no为5)的页面类型为FSP_TRX_SYS_PAGE_NO,记录了InnoDB重要的事务系统信息,包括持久化的最大事务ID,以及128个回滚段(代码中称为RSEG)的地址,double write位置等。记录于FSP_TRX_SYS_PAGE_NO的每个回滚段地址占8字节,格式为:
space id | 表空间ID |
---|---|
page no | 页面ID |
space id和page no各占4字节,二者指向的相应回滚段的管理页面,称为Rollback segment header。每个回滚段中有1024个Undo Slot,每个Undo Slot是一个存储Undo Log的页面的链表,其页面是通过《InnoDB文件结构解析》介绍过的段(segment)管理的。由于Insert Undo Log在事务提交之后,就可以直接删除,而Update Undo Log在事务提交之后,还需要满足其他事务对这些旧记录的可见性需求(MVCC),不能立刻删除,因此InnoDB将Insert Undo Log和Update Undo Log区分开,记录在不同的Undo page链表中。
事务申请一个新的Undo Slot就需要创建一个新的段的内存结构,为了避免频繁创建和释放段,回滚段的内存结构trx_rseg_t额外引入了两个cache链表:Insert Undo cached链表和Update Undo cached链表,分别用于回收只使用了一个Undo page,并且Undo page使用的空间小于整个页面空间的3/4的Insert Undo Slot或者Update Undo Slot。需要注意的是:当Undo Slot从Insert Undo cached链表被复用时,新的事务可以把之前事务的写入的Undo Log覆盖掉,从头开始写入新事务的Undo Log。而Undo Slot从Update Undo cached链表被复用时,旧的事务的Undo Log还需要服务于MVCC,新的事务不能覆盖之前事务写入的Undo Log,只能从旧Undo Log之后写入新事务的Undo Log。
如果Undo Slot不满足复用条件,Insert Undo Slot将被直接释放。记录了Update Undo Log的Undo Slot会被挂在本回滚段的History链表中(实际作为History链表的Node是Undo Log header中的TRX_UNDO_HISTORY_NODE属性,此内容将在3.3介绍),供MVCC使用。这些未被释放的Undo Slot将被Purge线程彻底删除,关于Undo Log Purge将在本文后续介绍。
接下来,详细了解Rollback segment header的结构:
名称 | 含义 |
---|---|
Fil_header | InnoDB页面的通用文件头 |
TRX_RSEG_MAX_SIZE | 本Rollback segment中管理的所有Undo Slot 链表持有的Undo Log页面最大值。该属性占4字节,默认值为4字节的最大值0xFFFFFFFF,而InnoDB页面默认16KB,也就是说默认每个回滚段能持有64 T的数据量。 |
TRX_RSEG_HISTORY_SIZE | History链表占用的页面数量 |
TRX_RSEG_HISTORY | History链表基节点 |
TRX_RSEG_FSEG_HEADER | 本回滚段对应段地址信息,通过段地址信息可以查到段的管理信息(iNode entry)。注意此处记录的是本回滚段的段地址,不是其中本回滚段中任何一个Undo Slot的段地址。【每个回滚段会使用1+1024个段】 |
TRX_RSEG_UNDO_SLOTS | 1024个Undo Slot的集合,占4*1024字节。Undo Slot如果被占用,则将Undo page链表段第一个页面的page no填入对应位置,否则填入FIL_NULL。 |
暂未使用 | |
Fil_trailer | InnoDB页面的通用文件尾 |
3.2 Undo Slot
每个Undo Slot都对应一个页类型为FIL_PAGE_UNDO_LOG的Undo page的链表,Undo page链表的页面的申请使用段(segment)来管理。FIL_PAGE_UNDO_LOG类型的页面的结构如下:
名称 | 含义 |
---|---|
Fil_header | Innodb页面的通用文件头 |
Undo page header | Undo page的独特结构 |
其他内容 | |
Fil_trailer | InnoDB页面的通用文件尾 |
Undo page header,其结构如下:
名称 | 含义 |
---|---|
TRX_UNDO_PAGE_TYPE | 页面要存储的Undo Log类型,可选值为TRX_UNDO_INSERT、TRX_UNDO_UPDATE,即Insert Undo Log或者Update Undo Log |
TRX_UNDO_PAGE_START | 表示在当前页面中是从什么位置开始存储Undo Log的,或者说表示第一条Undo日志在本页面中的起始偏移量。之所以有这个属性,是因为Undo Slot是可以复用的,如果Update Undo Slot被复用,那么新的事务的Undo Log将不是从头开始往后写 |
TRX_UNDO_PAGE_FREE | 与上面的TRX_UNDO_PAGE_START对应,表示当前页面中存储的最后一条Undo日志结束时的偏移量,或者说从这个位置开始,可以继续写入新的Undo Log |
TRX_UNDO_PAGE_NODE | 代表一个List Node结构。详细内容包括:Prev Node page number、Prev Node offset、Next Node page number、Next Node offset。通过本Node,可以将Undo page连成链表 |
Undo Slot的第一个页面,即Undo page链表的链表头与其后的Undo page少有不同,其在Undo page header之后,还有一个Undo Log segment header的结构。Undo Slot的第一个页面的结构如下所示:
名称 | 含义 |
---|---|
Fil_header | InnoDB页面的通用文件头 |
Undo page header | Undo page的独特结构 |
Undo Log segment header | Undo Slot第一个页面的特有结构 |
其他内容 | |
Fil_trailer | InnoDB页面的通用文件尾 |
其中Undo Log segment header的具体信息如下:
名称 | 含义 |
---|---|
TRX_UNDO_STATE | 本Undo page链表所处的状态 |
TRX_UNDO_LAST_LOG | 本Undo page链表中最后一个Undo Log header的位置,Undo Log header的概念将在下一节介绍 |
TRX_UNDO_FSEG_HEADER | 本Undo page链表对应的Undo Slot段地址 |
TRX_UNDO_PAGE_LIST | Undo page链表的基节点 |
TRX_UNDO_STATE可能的状态包括:
- TRX_UNDO_ACTIVE:活跃状态。也就是一个活跃的事务正在往这个段里边写入Undo日志。
- TRX_UNDO_CACHED:被缓存的状态。处在该状态的Undo页面链表等待着之后被其他事务复用。
- TRX_UNDO_TO_FREE:对于Insert Undo链表来说,如果在它对应的事务提交之后,该链表不能被复用,本Undo Slot将被释放,那么就会处于这种状态。
- TRX_UNDO_TO_PURGE:对于Update Undo链表来说,如果在它对应的事务提交之后,该链表不能被复用,那么就会处于这种状态,服务于MVCC,等待被Purge。
- TRX_UNDO_PREPARED:包含处于PREPARE阶段的事务产生的Undo日志,处于事务两阶段提交的过程中。
如第一部分Undo Log简介中所述,Undo page也受redo Log提供的原子性保护。考虑到普通表的Undo page修改需要写redo Log,而临时表的Undo page修改不需要写redo Log。因此,InnoDB中普通表和临时表的记录改动时产生的Undo Log要分别记录。由于Insert操作和Update操作的Undo也是分开记录的,因此一个事务可能需要最多四个Undo page链表,分别记录临时表的Insert Undo page链表、临时表的Update Undo page链表、普通表的Insert Undo page链表、普通表的Update Undo page链表,因此可能最多要占据4个Undo Slot。
3.3 Undo Log group
InnoDB规定同一个事务向同一个Undo page中写入的Undo Log算一个Undo Log group,在Undo Log group内所有Undo Log紧紧相连,中间没有任何空隙。在每个Undo Log group前,都有一个Undo Log header。Undo Log header中的信息很多,如下所示:
名称 | 含义 |
---|---|
TRX_UNDO_TRX_ID | 生成本组Undo日志的事务id |
TRX_UNDO_TRX_NO | 事务提交序号,使用此序号来标记事务的提交顺序(先提交的此序号小,后提交的此序号大),与MVCC相关 |
TRX_UNDO_DEL_MARKS | 标记本组Undo Log中是否包含由于Delete mark操作产生的Undo日志,包含的话除了清理Undo Log,还需到页面中彻底删除记录 |
TRX_UNDO_LOG_START | 表示本组Undo Log中第一条Undo日志的在页面中的偏移量 |
TRX_UNDO_XID_EXISTS | 本组Undo Log是否包含XID信息 |
TRX_UNDO_DICT_TRANS | 标记本组Undo Log是不是由DDL语句产生的 |
TRX_UNDO_TABLE_ID | 如果TRX_UNDO_DICT_TRANS为真,那么本属性表示DDL语句操作的表的Table Id |
TRX_UNDO_NEXT_LOG | 下一组的Undo Log在页面中开始的偏移量 |
TRX_UNDO_PREV_LOG | 上一组的Undo Log在页面中开始的偏移量 |
TRX_UNDO_HISTORY_NODE | List Node结构,作为History链表的节点。 |
3.4 Undo Log存储总结
下面以一个事务执行的过程为契机,总结Undo Log的储存:
事务刚开启时,不会申请回滚段以及其中的Undo Slot,只有事务在运行过程中运行了相应操作,才会去分配回滚段以及其中相应的Undo Slot。
事务在执行过程中,产生数据插入或更新操作时,需要到系统表空间中page no为5的FSP_TRX_SYS_PAGE_NO页面中申请回滚段。为了每个回滚段能均摊负载,回滚段采用round-robin(轮流循环)的方式分配给并发的事务。当仅仅对普通的记录做修改时,仅仅需要给事务分配普通表的回滚段,当仅仅对临时表做修改时,既会为事务分配临时表的回滚段,也会给事务分配普通表的回滚段。
事务获得回滚段,要分配新的Undo Slot时,会首先从回滚段的内存结构trx_rseg_t中的Insert Undo cached链表和Update Undo cached链表中找,如果有缓存的Undo Slot,那么就把这个缓存的Undo Slot分配给该事务,并复用旧的Undo Slot的段来分配页面。如果没有缓存的Undo Slot可供分配,那么就要到Rollback segment header页面中从前往后找未被使用的Undo Slot(值为FIL_NULL)来使用,找到后需要申请Undo Slot对应的段,并从中分配出第一个Undo Log页面,将page no填入Rollback segment header中对应Slot的位置。极端情况下,如果找不到需要分配的Undo Slot,则会给用户报Too many active concurrent transactions的错误,并回滚本事务。
一个事务最多可能需要4个Undo Slot,分别记录临时表的Insert Undo page链表、临时表的Update Undo page链表、普通表的Insert Undo page链表、普通表的Update Undo page链表。获取Undo Slot后,事务以组为单位写入Undo Log,写完一个Undo page后,再从Undo Slot的段里申请一个新页面,然后把这个页面插入到Undo页面链表中,继续往这个新申请的Undo page中写Undo Log。
事务提交以后,如果Undo Slot只有一个page,并且页面的使用空间不足3/4,则将Undo Slot挂在Insert Undo cached链表或者Update Undo cached链表中。Insert Undo Slot被复用后新的Insert Undo Log可以直接覆盖旧的Insert Undo Log,Update Undo Slot被复用后旧的Update Undo Log仍然需要服务于MVCC,新的Update Undo Log不能覆盖旧的Update Undo Log。如果Undo Slot不满足复用条件,那么Insert Undo Slot将被直接释放,而Update Undo Slot中的Undo Log header将按照trx_no的顺序挂在History list上,服务于MVCC。
4. Undo Log在事务回滚的使用(Rollback)
事务在运行过程中,用户可能会主动触发回滚。下面举一个简单的例子:
create table t (col1 int primary key, col2 char);
Insert into t values (1, 'a');
begin; // 事务1
Update t set col2 = 'b' where col = 1; // 更新1
Update t set col2 = 'c' where col = 1; // 更新2
rollback;
上述事务1中将先记录更新1的Undo Log,然后记录更新2的Undo Log。在回滚时,必须逆向操作才能顺利回到事务1开始的状态,即先回滚更新2,再回滚更新1,否则事务状态将出错。因此,事务回滚时,InnoDB会从最后一条Undo Log开始逆向将Undo Log apply到数据页面中。具体而言,InnoDB先通过从Undo Segment Header中记录TRX_UNDO_LAST_LOG找到当前事务的最后一个Undo Log header。通过Undo Log header可以找到其所在的Undo page的header,并在其中找到TRX_UNDO_PAGE_FREE。从TRX_UNDO_PAGE_FREE开始逆向apply Undo Log即可回滚事务。
5. Undo Log在多版本并发控制中的使用(MVCC)
在介绍MVCC之前,先简单介绍一下事务隔离级别。在SQL标准中,有四个隔离级别:
- READ UNCOMMITTED:未提交读。
- READ COMMITTED:已提交读。
- REPEATABLE READ:可重复读。
- SERIALIZABLE:可串行化。
SQL标准中,针对不同的隔离级别,并发事务可能发生不同严重程度的问题,具体情况如下:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | Possible | Possible | Possible |
READ COMMITTED | Not Possible | Possible | Possible |
REPEATABLE READ | Not Possible | Not Possible | Possible |
SERIALIZABLE | Not Possible | Not Possible | Not Possible |
READ UNCOMMITTED隔离级别的事务能直接读取其他未提交的事务修改过的记录,对并发的限制不大。SERIALIZABLE隔离级别的事务来说,所有记录的访问必须加速,可以改善的空间也不大。为了提高READ COMMITTED和REPEATABLE READ级别的读-写,写-读的并发能力。InnoDB提出了一致性读的概念,即通过多版本并发控制(MVCC)达到记录在被一个事务更新后,仍然能被另一个事务读取的效果。本部分将介绍其原理。
5.1 版本链
在《InnoDB行格式解析》中介绍过,InnoDB的行都包括以下两个隐藏列:
- trx_id:每次修改记录时,都会将修改的事务ID记录在此处。
- roll_ptr:该行更新前的Undo Log的地址,通过roll_ptr指向的Undo Log可以构造该行修改前的状态。
所有的roll_ptr属性能将该记录的所有历史版本串连成一个链表,称为版本链。
5.2 Readview
对于Read Committed和Repeatable Read来说,实现的关键在于如何判断事务的可见性。为此,InnoDB提出了Readview的概念。Readview可以认为是InnoDB某个时刻所有事务状态的快照。在介绍Readview的具体内容前,先介绍trx_id和trx_no的概念:
trx_id:事务id是一个不断递增的数字,当事务对某个表进行了增删改时,InnoDB就会为他分配一个独一无二的事务id。服务器在内存中会维护一个全局的事务id,每次为某个事务分配事务id后,就自增1。当这个变量是256的倍数时,该值就会持久化到系统表空间中page no为5的FSP_TRX_SYS_PAGE_NO页面中。当数据库重启时,会读取该持久化的事务id,并加上512,并继续分配事务ID。trx_id可以认为是为MVCC判断可见性服务的。
trx_no:trx_id用自增数字标识了事务的开始顺序,trx_no则是用来标识事务提交顺序的。在一个事务提交时,会为这个事务生成一个名为事务no的值,该值用来表示事务提交的顺序,先提交的事务no值小,后提交的事务的事务no值大。事务提交时,会把事务no值填入Undo Log header的trx_Undo_trx_no中。trx_no可以理解为判断Undo Log能否Purge服务的。关于trx_no的使用,将在Undo的清理中介绍。
了解了trx_id和trx_no的含义后,下面来了解Readview的主要内容:
名称 | 含义 |
---|---|
m_ids | 表示在生成Readview时当前系统中活跃的读写事务的事务id列表 |
min_trx_id | 表示在生成Readview时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。min_trx_id并不是源码中的名称,源码中的名称叫m_up_limit_id,此处叫min_trx_id是为了方便理解 |
max_trx_id | 表示生成Readview时系统中应该分配给下一个事务的id值。max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成Readview时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。max_trx_id并不是源码中的名称,源码中的名称叫m_low_limit_id,此处叫max_trx_id是为了方便理解 |
creator_trx_id | 表示生成该ReadView的事务的事务id |
m_low_limit_no | 此Readview创建时能看到的最大的trx_no |
5.3 可见性判断
有了Readview,在访问某条记录时只需要按照以下原则判断记录的某个版本是否可见:
- 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值大于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上面的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
Read Committed隔离级别的事务由于能实时看到已提交事务的信息,所以它每次查询前都需要更新自己的Readview。而Repeatable read隔离级别的事务要保证事务提交前数据是可重复读取的,所以只会在第一次执行查询语句时生成一个Readview,之后的查询就不会重复生成了。
6. Undo Log在崩溃恢复中的使用(Crash Recovery)
如第一部分Undo Log简介中所述,Undo Log也受redo Log提供的原子性保护。除了通用的一些MLOG_2BYTES、MLOG_4BYTES类型之外,Undo本身也有自己对应的redo Log类型:
- MLOG_UNDO_INIT类型在Undo page初始化的时候记录
- MLOG_UNDO_HDR_REUSE和MLOG_UNDO_HDR_CREATE:在分配Undo Log的时候,需要重用Undo Log header或需要创建新的Undo Log header的时候记录
- MLOG_UNDO_INSERT:在Undo Log里写入新的Undo Record时记录
- MLOG_UNDO_ERASE_END:Undo Log跨Undo page时抹除最后一个不完整的Undo Record的操作
在redo Log应用完成后,初始化完成数据词典子系统后,随即开始初始化事务子系统,而回滚段的初始化即在这一步完成。初始化回滚段时,会根据每个Undo Slot是否被使用、Undo Slot的状态、类型等信息来创建内存结构,并将Undo Slot插入回滚段内存结构trx_rseg_t中的Insert_Undo_list或者Update_Undo_list上。接着根据每个回滚段的Insert_Undo_list来恢复插入操作的事务,根据Update_Undo_list来恢复更新事务。如果既存在插入又存在更新,则只恢复一个事务对象。另外除了恢复事务对象外,还要恢复表锁及读写事务链表,从而恢复到崩溃之前的事务场景。
当从Undo恢复崩溃前活跃的事务对象后,会去开启一个后台线程来做事务回滚和清理操作,对于处于TRX_UNDO_ACTIVE状态的事务直接回滚,对于既不TRX_UNDO_ACTIVE也非TRX_UNDO_PREPARED状态的事务,包括TRX_UNDO_CACHED、TRX_UNDO_TO_FREE和TRX_UNDO_TO_PURGE,直接则认为其是提交的,直接释放事务对象。完成这一步后,理论上事务链表上只存在PREPARE状态的事务。
随后进入XA Recover阶段,MySQL使用内部XA,即通过BinLog和InnoDB做XA恢复。在初始化完成引擎后,Server层会开始扫描BinLog文件中记录的XID。由于BinLog rotate时都会保证前一个BinLog文件中的事务已经提交并且redo Log已经sync到磁盘,所以只需要扫描最后一个BinLog文件即可。比较BinLog文件中XID和InnoDB层的事务XID,如果如果XID已经存在于binLog中了,对应的事务需要提交;否则需要回滚事务。
7. Undo的清理(Purge)
InnoDB在Undo Log中保存了多份历史版本来实现MVCC,当某个历史版本已经确认不会被任何现有的和未来的事务看到的时候,就应该被清理掉。因此就需要有办法判断哪些Undo Log不会再被看到。
如5.2 Readview部分所述,InnoDB每一个写事务提交时都会被分配一个递增的编号trx_no作为事务的提交序号,并记录在Undo Log header中的TRX_UNDO_TRX_NO中。每个读事务会在自己的ReadView中记录自己开始的时候看到的最大的trx_no为m_low_limit_no。那么,如果一个事务的trx_no小于当前所有活跃的读事务Readview中的这个m_low_limit_no,说明这个事务在所有的读开始之前已经提交了,其修改的新版本是可见的, 因此不再需要通过Undo构建之前的版本,这个事务的Undo Log也就可以被清理了。
Undo Log group按提交顺序挂在回滚段的History list中,History list是按照trx_no排序的。因此当Purge线程执行Purge操作时,总是取最老的Readview(如果当前没有Readview则现场生成一个)中的m_low_limit_no去与回滚段History list中最小的Undo Log的trx_no对比,如果Undo Log的trx_no小于 老的Readview的m_low_limit_no,则清除对应的Undo Log。清除Undo Log时,如果发现Undo Log header中的TRX_UNDO_DEL_MARKS为true,说明其中有待彻底删除的记录。还需要到记录页面中将记录彻底删除。
最老的Readview决定了哪些undo log可以Purge。如果存在一个Repeatable read隔离级别的大事务久未提交,那么事务开启时的Readview将始终保留,Purge操作无法进行,系统的Undo Log不断增多,版本链不断加长,被标记delete mark后等待被彻底删除的记录也不断增加,系统的性能将受影响。
8 参考文献
- http://mysql.taobao.org/monthly/2015/04/01/
- http://mysql.taobao.org/monthly/2021/10/01/
- https://relph1119.github.io/mysql-learning-notes/#/mysql/22-%E5%90%8E%E6%82%94%E4%BA%86%E6%80%8E%E4%B9%88%E5%8A%9E-undo%E6%97%A5%E5%BF%97%EF%BC%88%E4%B8%8A%EF%BC%89
- https://relph1119.github.io/mysql-learning-notes/#/mysql/23-%E5%90%8E%E6%82%94%E4%BA%86%E6%80%8E%E4%B9%88%E5%8A%9E-undo%E6%97%A5%E5%BF%97%EF%BC%88%E4%B8%8B%EF%BC%89
- https://relph1119.github.io/mysql-learning-notes/#/mysql/24-%E4%B8%80%E6%9D%A1%E8%AE%B0%E5%BD%95%E7%9A%84%E5%A4%9A%E5%B9%85%E9%9D%A2%E5%AD%94-%E4%BA%8B%E5%8A%A1%E7%9A%84%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E4%B8%8EMVCC