Mysql - undo日志
1、事务回滚的需求
回滚(rollback):撤销对事务的操作,保证事务的原子性
undo日志(undo log):记录回滚内容的文件
2、事务id
当事务对某个表进行增、删、改操作时,会为该事务分配一个独一无二的事务id。先被分配id的事务得到的是较小的事务id,后被分配id的事务得到的是较大的事务id。
3、undo日志的类型
一般每对一条记录做一次改动,就对应着一条(更新记录可能对应2条)undo日志。
undo日志会从0开始编号,按照生成的顺序被称为第0号undo日志、第1号undo日志、…、第n号undo日志等,因此该编号也被称为undo no。
undo日志会被存放到到类型为FIL_PAGE_UNDO_LOG的页面中。
记录会被存放到类型为FIL_PAGE_UNDO_LOG的页面中。

因此,从图中可以看出,roll_pointer隐藏列本质就是一个指针,指向记录对应的undo日志。trx_id隐藏列是某个对这个聚簇索引记录做改动的语句所在的事务对应的事务id。
日志的主要类型有三种:
- 插入操作类型的undo日志
- 删除操作类型的undo日志
- 修改操作类型的undo日志
3.1 Insert操作对应的undo日志

向表中插入两条记录:
INSERT INTO demo18(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');
则会产生两条类型为TRX_UNDO_INSERT_REC的undo日志。
-
第一条undo日志的
undo no为0,记录主键占用的存储空间长度为4,真实值为1。

-
第二条undo日志的
undo no为1,记录主键占用的存储空间长度为4,真实值为2。

3.2 Delete操作对应的undo日志
插入到页面中的记录会根据记录头信息中的next_record属性组成一个单向链表,这个链表称之为正常记录链表;被删除的记录也会根据记录头信息中的next_record属性组成一个链表,只不过这个链表中的记录占用的存储空间可以被重新利用,所以也称这个链表为垃圾链表。
PageHeader部分有一个称之为PAGE_FREE的属性,它指向由被删除记录组成的垃圾链表中的头节点。假设此刻某个页面中的记录分布情况是这样的:

从图中可以看出,正常记录链表中包含了3条正常记录,垃圾链表里包含了2条已删除记录,在垃圾链表中的这些记录占用的存储空间可以被重新利用。页面的Page Header部分的PAGE_FREE属性的值代表指向垃圾链表头节点的指针。
假设现在准备使用DELETE语句把正常记录链表中的最后一条记录给删除掉,则这个删除的过程需要经历两个阶段:
- 阶段一:仅仅将记录的delete_mask标识位设置为1,其他的不做修改(其实记录的trx_id、roll_pointer这些隐藏列的值也会被修改)。InnoDB把这个阶段称之为
delete mark。

可以看到,正常记录链表中的最后一条记录的delete_mask值被设置为1,但是并没有被加入到垃圾链表。也就是此时记录处于一个中间状态,在删除语句所在的事务提交之前,被删除的记录一直都处于这种所谓的中间状态。
- 阶段二:当该删除语句所在的
事务提交之后,会有专门的线程来真正的把记录删除掉。所谓真正的删除就是把该记录从正常记录链表中移除,并且加入到垃圾链表中,同时修改PAGE_FREE属性的值。InnoDB的把这个阶段称之为purge。
当阶段二执行结束,这条记录就真正的被删除掉了。这条已删除记录占用的存储空间也可以被重新利用了。

针对delete_mask阶段,InnoDB设计了一种称之为TRX_UNDO_DEL_MARK_REC类型的undo日志,它的完整结构如下图所示:

3.3 Update操作对应的undo日志
3.3.1 不更新主键
-
就地更新:更新前后的列所占用的存储空间一样大
-
先删除(真正删除)掉旧纪录,再插入新纪录:更新前后的列所占用的存储空间不一样大

3.3.2 更新主键
-
将旧记录进行
delete mark操作 -
根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中(需重新定位插入的位置)
4、FIL_PAGE_UNDO_LOG页面
-
FIL_PAGE_UNDO_LOG类型页面的通用结构

-
Undo Page Header结构

其中各属性意思如下:
-
TRX_UNDO_PAGE_TYPE:本页面准备存储什么种类的undo日志–
TRX_UNDO_INSERT:类型为TRX_UNDO_INSERT_REC的undo日志属于此大类
–TRX_UNDO_UPDATE:除了类型为TRX_UNDO_INSERT_REC的undo日志,其他类型的undo日志都属于这个大类 -
TRX_UNDO_PAGE_START:表示第一条undo日志在本页面中的起始偏移量 -
TRX_UNDO_PAGE_FREE:表示当前页面中存储的最后一条undo日志结束时的偏移量 -
TRX_UNDO_PAGE_NODE:代表一个List Node结构
5、Undo页面链表
一个事务可能包含多个语句,而且一个语句可能对若干条记录进行改动,而对每条记录进行改动前,都需要记录1条或2条的undo日志,所以在一个事务执行过程中可能产生很多undo日志,这些日志可能一个页面放不下,需要放到多个页面中,这些页面就通过TRX_UNDO_PAGE_NODE属性连成了链表:

一个事务中最多包含4个以undo页面为节点组成的链表,并且这4个链表并非在事务开始时直接分配,而是有需求则分配。

6、Undo Log Segment
InnoDB的规定,每一个Undo页面链表都对应着一个段,称之为 Undo Log Segment。也就是说链表中的页面都是从这个段里边申请的,所以他们在Undo页面链表的第一个页面,也就是上边提到的first undo page中设计了一个称之为Undo Log Segment Header的部分,这个部分中包含了该链表对应的段的segment header信息以及其他的一些关于这个段的信息,所以Undo页面链表的第一个页面其实长这样:

其中,Undo Log Segment Header部分结构如下:

其中各个属性的意思如下:
-
TRX_UNDO_STATE:本Undo页面链表处在什么状态。一个Undo Log Segment可能处在的状态包括如下:–
TRX_UNDO_ACTIVE:活跃状态,也就是一个活跃的事务正在往这个段里边写入undo日志。–
TRX_UNDO_CACHED:被缓存的状态。处在该状态的Undo页面链表等待着之后被其他事务重用。–
TRX_UNDO_TO_FREE:对于insert undo链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。–
TRX_UNDO_TO_PURGE:对于update undo链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。–
TRX_UNDO_PREPARED:包含处于PREPARE阶段的事务产生的undo日志 -
TRX_UNDO_LAST_LOG:本Undo页面链表中最后一个Undo Log Header的位置。 -
TRX_UNDO_FSEG_HEADER:本Undo页面链表对应的段的Segment Header信息(就是我们上一节介绍的那个10字节结构,通过这个信息可以找到该段对应的INODE Entry) -
TRX_UNDO_PAGE_LIST:Undo页面的Undo Page Header部分有一个12字节大小的TRX_UNDO_PAGE_NODE属性,这个属性代表一个List Node结构,这些页面就可以通过这个属性连成一个链表。TRX_UNDO_PAGE_LIST属性代表着这个链表的基节点。
Undo Log Header具体的结构如下:

其中各个属性的意思如下:
-
TRX_UNDO_TRX_ID:生成本组undo日志的事务id -
TRX_UNDO_TRX_NO:事务提交后生成的一个需要序号,使用此序号来标记事务的提交顺序(先提交的此序号小,后提交的此序号大)。 -
TRX_UNDO_DEL_MARKS:标记本组undo日志中是否包含由于Delete mark操作产生的undo日志。 -
TRX_UNDO_LOG_START:表示本组undo日志中第一条undo日志的在页面中的偏移量。 -
TRX_UNDO_XID_EXISTS:本组undo日志是否包含XID信息。 -
TRX_UNDO_DICT_TRANS:标记本组undo日志是不是由DDL语句产生的。 -
TRX_UNDO_TABLE_ID:如果TRX_UNDO_DICT_TRANS为真,那么本属性表示DDL语句操作的表的table id。 -
TRX_UNDO_NEXT_LOG:下一组的undo日志在页面中开始的偏移量。 -
TRX_UNDO_PREV_LOG:上一组的undo日志在页面中开始的偏移量。 -
TRX_UNDO_HISTORY_NODE:一个12字节的List Node结构,代表一个称之为History链表的节点。
综上,一个完整的undo页面链表示意图如下:

7、重用undo页面
重用undo页面的条件:
-
该链表中只包含一个Undo页面
-
该Undo页面已经使用的空间小于整个页面空间的3/4
-
insert undo链表

-
update undo链表

-
8、回滚段
8.1 回滚段的概念
为了更好的管理undo页面链表,InnoDB又设计了一个称之为Rollback Segment Header的页面,在这个页面中存放了各个Undo页面链表的frist undo page的页号,他们把这些页号称之为undo slot。
InnoDB规定,每一个Rollback Segment Header页面都对应着一个段,这个段就称为Rollback Segment,也就是回滚段,这个Rollback Segment里只有一个页面。

其中各个属性的含义如下:
-
TRX_RSEG_MAX_SIZE:本Rollback Segment中管理的所有Undo页面链表中的Undo页面数量之和的最大值。 -
TRX_RSEG_HISTORY_SIZE:History链表占用的页面数量。 -
TRX_RSEG_HISTORY:History链表的基节点。 -
TRX_RSEG_FSEG_HEADER:本Rollback Segment对应的10字节大小的Segment Header结构,通过它可以找到本段对应的INODE Entry。 -
TRX_RSEG_UNDO_SLOTS:各个Undo页面链表的first undo page的页号集合,也就是undo slot集合。
8.2 从回滚段中申请undo页面链表
初始情况下,由于未向任何事务分配任何Undo页面链表,所以对于一个Rollback Segment Header页面来说,它的各个undo slot都被设置成了一个特殊的值:FIL_NULL(对应的十六进制就是0xFFFFFFFF),表示该undo slot不指向任何页面。
当开始有事务需要分配Undo页面链表了,就从回滚段的第一个undo slot开始,看看该undo slot的值是不是FIL_NULL:
-
如果是
FIL_NULL,那么在表空间中新创建一个段(也就是Undo Log Segment),然后从段里申请一个页面作为Undo页面链表的first undo page,然后把该undo slot的值设置为刚刚申请的这个页面的页号,这样也就意味着这个undo slot被分配给了这个事务。 -
如果不是
FIL_NULL,说明该undo slot已经指向了一个undo链表,也就是说这个undo slot已经被别的事务占用了,那就跳到下一个undo slot,判断该undo slot的值是不是FIL_NULL,重复上边的步骤。
一个Rollback Segment Header页面中包含1024个undo slot,如果这1024个undo slot的值都不为FIL_NULL,这就意味着这1024个undo slot都已经被分配给了某个事务,此时由于新事务无法再获得新的Undo页面链表,就会回滚这个事务并且给用户报错。
当一个事务提交时,它所占用的undo slot有两种命运:
-
如果该undo slot指向的Undo页面链表符合被重用的条件,该undo slot就处于
被缓存的状态,InnoDB规定这时该Undo页面链表的TRX_UNDO_STATE属性会被设置为TRX_UNDO_CACHED。被缓存的undo slot都会被加入到一个链表,根据对应的Undo页面链表的类型不同,也会被加入到不同的链表:
-
如果对应的Undo页面链表是insert undo链表,则该undo slot会被加入
insert undo cached链表。 -
如果对应的Undo页面链表是update undo链表,则该undo slot会被加入
update undo cached链表。
如果有新事务要分配
undo slot时,先从对应的cached链表中找。如果没有被缓存的undo slot,才会到回滚段的Rollback Segment Header页面中再去找。 -
-
如果该undo slot指向的Undo页面链表不符合被重用的条件,那么针对该undo slot对应的Undo页面链表类型不同,也会有不同的处理:
-
如果对应的Undo页面链表是
insert undo链表,则该Undo页面链表的TRX_UNDO_STATE属性会被设置为TRX_UNDO_TO_FREE,之后该Undo页面链表对应的段会被释放掉,然后把该undo slot的值设置为FIL_NULL。 -
如果对应的Undo页面链表是
update undo链表,则该Undo页面链表的TRX_UNDO_STATE属性会被设置为TRX_UNDO_TO_PRUGE,则会将该undo slot的值设置为FIL_NULL,然后将本次事务写入的一组undo日志放到所谓的History链表中。
-
8.3 多个回滚段

8.4 回滚段的分类
-
第0号、第33~127号回滚段属于一类:其中第0号回滚段必须在系统表空间中(就是说第0号回滚段对应的Rollback Segment Header页面必须在系统表空间中),第33~127号回滚段既可以在系统表空间中,也可以在自己配置的undo表空间中。
-
第1~32号回滚段属于一类。这些回滚段必须在临时表空间(对应着数据目录中的ibtmp1文件)中。
8.5 事务分配Undo页面链表详细过程
graph TD
A[为事务分配回滚段] --> C{查看Cached链表有没有缓存的undo slot}
C -- 有 --> D[直接分配]
C -- 无 --> E[Rollback Segment Header页面中找undo slot分配]
E --> F[从Undo Log Segment中申请一个页面作为Undo页面链表的first undo page]
F --> G[把undo日志写入到Undo页面链表]
D --> G




