undo log有两个作用:提供回滚和多个行版本控制(MVCC)

1、事务id

一个事务可以使一个只读事务,也可以是一个读写事务。


分配事务id的时机

对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增删改操作时,才会为这个事务分配一个事务id,否则是不分配事务id的。
对于读写事务来说。只有在它第一次对某个表(包括用户创建的临时表)执行增删改操作时,才会为这个事务分配一个事务id,否则是不分配事务id的。


事务id是怎么生成的

  • 服务器会在内存维护一个全局变量,每当需要为某个事务分配事务id时,就会把该变量的值作为事务id分配给该事务,并且把该变量自增1
  • 每当这个变量为256的倍数时,就会将该变量的值刷新到系统表空间在页号为5的页面中一个名为Max Trx ID的属性中,这个属性占用8字节的存储空间
  • 当系统下一次重启时,会将这个Max Trx ID属性加载到内存,并将该值加上256后赋给之前提到的分配事务id的全局变量

行记录隐藏列

trx_id就是对该行记录进行增删改操作所在事务对应的事务id

列名 是否必须 占用空间 描述
row_id 6字节 优先使用用户自定义主键,如果没有自定义主键,则选择一个不允许存储null值的唯一索引列作为键,如果表中没有不允许存储null值的唯一索引列,则InnoDB会为表默认添加一个名为row_id的隐藏列做主键。
trx_id 6字节 事务id
roll_pointer 7字节 回滚指针

2、UNDO页面

在InnoDB数据页中说过FIL_PAGE_UNDO_ LOG 类型页用来存储undo log。

点击查看【processon】

Undo Page Header属性

属性名 长度(字节) 描述
TRX_UNDO_PAGE_TYPE 2 undo日志类型,同一个undo页面只能存储一种大类的undo日志
TRX_UNDO_PAGE_START 2 第一条Undo日志在本页面的起始偏移量
TRX_UNDO_PAGE_FREE 2 最后一条Undo日志在本页面的结束偏移量,或者说本页面下一条undo 日志的起始位置
TRX_UNDO_PAGE_NODE 12 一个链表节点结构

日志类型分类

  • TRX_UNDO_INSERT(使用十进制1表示): 一般由insert语句产生,当update语句有更新主键的情况时也会产生此类型的undo日志。TRX_UNDO_INSERT的undo日志在事务提交后可以直接删除。
  • TRX_UNDO_UPDATE(使用十进制2表示):除了TRX_UNDO_INSERT类型的undo日志,其他类型的undo日子都属于这个大类。TRX_UNDO_UPDATE类型的undo日志要为mvcc服务,不能直接删掉。

3、UNDO页面链表

3.1、单个事务的Undo页面链表

一个事务执行过程中可能产生很多undo日志,这些日志可能在一个页面放不下,需要放在多个页面中,这些页面通过**TRX_UNDO_PAGE_NODE**属性连成了链表

InnoDB规定,在对普通表和临时表的记录改动时锁产生的undo日志要分别记录,同时一个undo页面要么存储TRX_UNDO_INSERT大类的日志,要么存储TRX_UNDO_UPDATE大类的日志。而在一个事务中,可能混合着INSERT,UPDATE,DELETE语句,意味着产生不同类型的日志。所以在一个事务中最多有4个以UNDO页面为节点组成的链表。

点击查看【processon】

链表分配策略:

  • 刚开启事务时,一个Undo页面链表也不分配
  • 当事务执行过程中对普通表插入记录或者执行更新记录主键的操作后,就会为其分配一个普通表的insert undo链表
  • 当事务执行过程中删除或者更新了普通表中的记录后,就会为其分配一个普通表的update undo 链表
  • 当事务执行过程中对临时表插入记录或者执行更新记录主键的操作后,就会为其分配一个临时表的insert undo链表
  • 当事务执行过程中删除或者更新了临时表中的记录后,就会为其分配一个临时表的update undo 链表

3.2、多个事务的Undo页面链表

为了尽可能提高undo日志的写入效率,不同事务产生的undo日志需要写入不同的Undo页面链表。


比如:现在有两个事务,分别称之为trx1和trx2

  • trx1对普通表执行了insert操作,对临时表执行了insert和update操作
  • trx2对普通表执行了insert,update,delete操作,没有对临时表进行操作

trx1和trx2分配链表如图所示:
点击查看【processon】

4、Undo 页面链表第一个页面的特殊部分

对于没有被重用的Undo页面链表来说,链表的第一个页面在真正写入undo 日志前,会填充Undo Page Header,Undo Log Segment Header,Undo Log Header这3个部分,之后才会正式写入undo日志,对于其他页面来说,在真正写入undo日志前只会填充Undo Page Header

4.1、Undo Log Segment Header

InnoDB规定每个Undo页面链表都对应一个段,称为Undo Log Segment,undo链表的页面都是从这个段中申请的,所以在Undo页面链表的第一个页设计了一个名为Undo Log
Segment Header的部分

点击查看【processon】

属性名 长度(字节) 描述
TRX_UNDO_STATE 2 本Undo页面链表处于什么状态
TRX_UNDO_LAST_LOG 2 本Undo页面链表中最后一个Undo Log Header的位置
TRX_UNDO_FSEG_HEADER 10 本Undo页面链表对应的段Segment Header信息
TRX_UNDO_PAGE_LIST 16 Undo页面的链表的基节点

TRX_UNDO_STATE说明:
TRX_UNDO_ACTIVE:活跃状态,也就是一个活跃事务正在向这个Undo页面链表写入undo日志
TRX_UNDO_CACHED:被缓存状态,处于该状态的Undo页面链表等待后被其他事务重用
TRX_UNDO_TO_FREE:等待被释放的状态,对于insert undo链表来说,如果在它对应的事务提交后,该链表不能重用,那么就会处于这种状态
TRX_UNDO_TO_PURGE:等待被purge状态,对应update undo链表来说,如果它对应的事务提交后,该链表不能重用,那么就会处于这种状态
TRX_UNDO_PREPARED:处于此状态的Undo页面链表用于存储处于PREPARE阶段的事务产生的日志

4.2、Undo Log Header

同一个事务向undo页面链表中写入的undo日志算是一组,在写入每组undo日志时,都会在这组undo日志前先记录下关于这个组的一些属性,存储这些属性的地方称为Undo Log Header。Undo Log Header位于undo页面链表第一个页面,在Undo Log Segment Header下。

点击查看【processon】

一般来说,一个Undo页面链表只存储一个事务执行过程中产生的一组undo日志。但是在某些情况下,后续开启的事务会复用这个Undo 页面链表,这就会导致一个Undo页面链表中可能存放多组undo日志。TRX_UNDO_NEXT_LOG和TRX_UNDO_PREV_LOG就是用来标记下一组和上一组undo日志在页面中的偏移量的。

属性名 长度(字节) 描述
TRX_UNDO_TRX_ID 8 生成本组undo日志的事务id
TRX_UNDO_TRX_NO 8 事务提交后生成的一个序号,此序号用来标记事务提交的顺序(先提交的序号小,后提交的序号大)
TRX_UNDO_DEL_MARKS 2 标记本组undo日志中是否包含由delete mark操作产生的undo日志
TRX_UNDO_LOG_START 2 表示本组undo日志中第一条undo日志在页面的偏移量
TRX_UNDO_XID_EXISTS 1 本组undo日志是否包含XID信息
TRX_UNDO_DICT_TRANS 1 标记本组undo日志是不是由DDL语句产生
TRX_UNDO_TABLE_ID 8 如果TRX_UNDO_DICT_TRANS为真,那么本属性表示DDL语句操作的表的table_id
TRX_UNDO_NEXT_LOG 2 下一组undo日志在页面中起始偏移量
TRX_UNDO_PREV_LOG 2 上一组undo日志在页面中起始偏移量
TRX_UNDO_HISTORY_NODE 12 一个12字节的链表节点结构,代表一个名为History链表的节点

5、重用Undo页面

某些事务在执行时可能只修改了一条或者几条记录,针对某个Undo页面值产生了非常少的Undo日志,这些Undo日志可能值占用了很少的存储空间,这样每开启一个事务就创建一个Undo页面链表来存储一点undo日志很浪费。

于是InnoDB规定事务提交后在满足以下情况下可以重用该事务的Undo 页面链表:

  • 该链表只包含一个Undo页面
  • 该Undo页面已经使用的空间小于整个页面的3/4

insert Undo 页面链表和 update Undo 页面链表在重用时策略是不同的:

insert undo 链表
事务提交后,insert undo链表中的undo日志可以直接覆盖
点击查看【processon】

update undo 链表

事务提交后,update undo链表中的undo日志不能立即删除,这些日志会用于mvcc。之后的事务想重用update undo链表,就必须保留之前事务写入的undo日志。

点击查看【processon】

6、回滚段

6.1、回滚段概念

在同一时刻不同事务的Undo页面链表是不一样的,系统在同一时刻存在很多个Undo页面链表。为了更好的管理这些链表,InnoDB设计了一个名为 Rollback Segment Header的页面,这个页面存放了各个Undo 页面链表的first undo page(页面链表的第一个页面)的页号,这些页号称为undo slot

InnoDB规定每一个Rollback Segment Header页面都对应着一个回滚段,而回滚段只有一个页面(Mysql5.7.22版本,后续版本可能会增加页面)。
点击查看【processon】

Rollback Segment Header页面属性:

属性名 长度(字节) 描述
TRX_RSEG_MAX_SIZE 4 回滚段中管理的所有Undo页面链表中Undo页面数量之和最大值,即回滚段中所有Undo页面链表的Undo页面之和不能超过TRX_RSEG_MAX_SIZE指定的值,默认无限大,即4个字节表示的最大值,但是0XFFFFFFFF有特殊用途,所以最大值为0XFFFFFFFE
TRX_RESG_HISTORY_SIZE 4 History链表占用的页面数量
TRX_RSEG_HISTORY 16 History链表的基节点
TRX_RESG_FSEG_HEADER 10 这个回滚段对应10字节大小的Segment Header结构
TRX_RESG_UNDO_SLOTS 4096 各个Undo页面链表的 first undo page(页面链表的第一个页面)的页号集合,也就是undo slot集合

6.2、从回滚段中申请Undo页面链表

在初始情况下,由于未向任何事务分配任何Undo页面链表,所以对于一个Rollback Segment Header页面来说,它的各个undo slot 都被设置成一个特殊的值:FIL_NULL(对应的16进制是0XFFFFFFFF),这表示该undo slot不指向任何页面。
随着时间流逝开始有事务需要分配undo 页面链表了,于是从回滚段的第一个undo slot开始,看看该undo slot的值是否为FIL_NULL。

  1. - 如果是FIL_NULL,那么就从表空间中新创建一个段(也就是Undo Log Segment),然后从段中申请一个页面作为Undo页面链表的first undo page,最后把该undo slot的值设置为刚刚申请的这个页面的地址,这就意味着这个undo slot被分配给了这个事务。
  2. - 如果不是FIl_NULL,就说明该undo slot已经指向了一个undo链表,也就是说这个undo slot已经被其它的事务占用了,这就需要跳向下一个undo slot,判断该slot的值是否为FIL NULL,并重复以上步骤。

当一个事务提交时,它所占用的undo slot 有两种命运:

  - **如果该undo slot 指向的undo 页面链表符合被重用的条件**(即Undo页面链表只使用了一个页面,且已使用空间小于该页面的3/4),该undo slot就处于被缓存状态。该Undo 页面链表的**TRX_UNDO_STATE**属性(该属性在Undo页面链表第一个页面的Undo Log Segment Header部分)设置为**TRX_UNDO_CACHED**。
     - 如果对应的Undo页面链表为 insert undo链表,则该undo slot会被加入insert undo cached链表中
     - 如果对应的Undo页面链表为 update undo链表,则该undo slot会被加入update undo cached链表中

如果有新事物需要分配undo slot,都优先从对应的cache链表中找,如果没有被缓存的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_PURGE,**并将该undo slot的值设置为FIL_NULL,然后将本次事务写入的一组undo日志放到History链表中(这里并不会将Undo页面对应的段给释放掉,因为这些undo日志还需要为MVCC服务)

6.3、回滚段的管理与分类

InnoDB定义了128个回滚段,每个回滚段有1024个undo slot。每个回滚段对应一个 Rollback Segment Header页面,128个回滚段则对应128个页面地址,为了方便管理,InnoDB在系统表空间第5号页面的某个区域设计了128个8字节大小的格子。

image.png
每个8字节格子由两部分组成:

  - **4字节的Space ID,代表一个表空间的ID**
  - **4字节的Page number,代表一个页号**

8字节的数据相当于一个指针,指向了Rollback Segment Header页面,此处还需要表空间id,说明不同的回滚段可能分布在不同的表空间。


回滚段分类
第0号、第33~127号回滚段属于一类,负责事务在执行过程中对普通表改动分配Undo页面链表。其中第0号回滚段必须在系统表空间中,第33~127号回滚段既可以在系统表空间也可以在自己配置的undo表空间中。
第1~32号回滚段属于一类,负责事务在执行过程中对临时表改动分配Undo页面链表,这些回滚段必须在临时表空间

6.4、行记录隐藏列roll_pointer的组成

行记录中包含一个名为roll_pointer的隐藏列,这个属性本质上是一个指针,指向一条undo 日志的地址。

点击查看【processon】

属性名 长度(比特) 描述
is_insert 1 该指针指向的undo日志是否是TRX_UNDO_INSERT大类的undo日志
rseg_id 7 表示该指针指向的undo日志的回滚段编号,编号范围为0~127
page number 32 表示该指针指向undo日志在所在页面的页号
offset 16 表示该指针指向undo日志在在页面中的偏移量

7、为事务分配Undo页面链表的详细过程

1、事务在执行过程中对普通表记录进行首次改动之前,首先会到系统表空间的第5号页面中分配一个回滚段(其实就是获取一个Rollback Segment Header页面的地址)。一旦某个回滚段被分配给了这个事务,那么之后该事务再对普通表的记录进行修改时,就不用重复分配了。为事务分配回滚段时使用循环分配的方式,比如当前事务分配了0号回滚段,下一个事务就要分配33号回滚段。


2、在分配到回滚段后,首先查看回滚段的两个cached链表是否有缓存的undo slot。如果事务执行的insert操作,则去回滚段对应的insert undo cached链表查看,如果事务执行的update操作,则去回滚段对应的update undo cached链表中查看。如果有缓存的undo slot,则将缓存的undo slot分配给该事务。如果没有缓存的undo slot可供分配,那么就要到Rollback Segment Header页面中找一个可用的undo slot分配给当前事务(如果1024个undo slot都不是FIL_NULL,就直接报错)。


3、找到可用的undo slot后,如果该undo slot是从cached链表中获取的,那么对应的Undo Log Segment就已经分配了;否则需要重新分配一个Undo Log Segment,然后从该Undo Log Segment中申请一个页面作为undo 页面链表的first undo page(undo页面链表的第一个页面),并把该页的页号填入undo slot中。


4、然后事务就可以把undo日志写到上面申请的undo页面链表中了

8、回滚段相关配置

配置回滚段数量

系统中一共有128个回滚段,这个是默认值,可以通过启动项innod_rollback_segments 来配置回滚段的数量,可配置的范围是1~128,但是该选项并不会影响临时表的回滚段数量,针对临时表的回滚段数量一直是32。
如果把innod_rollback_segments 的值设置为1~33之间的数,那么则会有1个针对普通表的回滚段,32个针对临时表的可用回滚段
如果把innod_rollback_segments 的值设置为大于33的数,那么针对普通表的回滚段个数为innod_rollback_segments的值-32,32个针对临时表的可用回滚段


配置undo表空间

默认情况下针对普通表设立的回滚段(第0号以及第33~127号回滚段)都是被分配到系统表空间中的,其中第0号回滚段一直在系统表空间,但是第33~127号回滚段可以通过配置放到自定义的undo表空间中。但是这种配置只有在系统初始化(创建数据目录时)时使用,一旦初始化完成就不能修改了。

  • 通过innodb_undo_directory指定undo表空间所在目录,如果没有指定该参数,则默认undo表空间所在目录就为数据目录
  • 通过innodb_undo_tablespaces定义undo表空间的数量,该参数默认值为0,表明不创建任何undo表空间

设立undo表空间的好处就是undo表空间的文件大到一定程度后,可以自动将该undo表空间截断成一个小文件,而系统表空间的大小只能不断增大,不能截断。

9、undo 日志在崩溃恢复时的作用

mysql崩溃使用redo log恢复时,存在一个问题,那就是那些没有提交的事务写的redo log也有可能刷盘,那么这些未提交事务修改过的页面在mysql服务器重启时也可能恢复了。
为了保证事务的原子性,有必要在服务器重启时将这些未提交的事务回滚掉,通过undo log可以找到这些未提交的事务。

首先可以通过系统表空间的第5号页面定位到128个回滚段的位置,在每一个回滚段中找到哪些值不为FIL_NULL的undo slot,每个undo slot对应着一个undo 页面链表。然后从undo页面链表的第一个页面的Undo Log Segment Header中找到TRX_UNDO_STATE属性,该属性标识当前Undo页面链表所处的状态。如果该属性值为TRX_UNDO_ACTIVE活跃状态,说明有一个活跃的事务正在向该undo 页面链表写入undo日志。然后在Undo Log Segment Header中找到
TRX_UNDO_LAST_LOG属性,通过该属性能找到本undo页面链表最后一个Undo Log Header位置,从该Undo Log Header中可以找到对应事务id以及一些其它信息,则该事务id对应的事务为未提交事务。通过undo log中记录的信息将该事务对页面的修改全部回滚掉,这样就保证了事务的原子性。