1. 前言
为了实现事务的回滚和MVCC,InnoDB设计了Undo log模块,简单来说就是在修改记录前先记下日志,以便之后能将记录恢复成修改前的样子。针对insert、delete、update这三种不同的操作,InnoDB设计了不同类型的undo log,每个类型的undo log都有自己的格式,里面记录了撤销记录修改所必须的数据,InnoDB可以根据undo log将记录进行恢复。
和redo log一样,undo log也是要持久化到磁盘的,来看看InnoDB是如何存储undo log的吧。
2. FIL_PAGE_UNDO_LOG页
undo log和用户记录一样存在一个个页中,存储undo log的页面的类型是FIL_PAGE_UNDO_LOG
,简称undo log页。
File Header和File Trailer是通用页结构了,不再赘述,重点关注Undo Page Header:
- TRX_UNDO_PAGE_TYPE :页面存储的undo log种类。
undo log分为两大类,分别是TRX_UNDO_INSERT
和TRX_UNDO_UPDATE
,前者代表记录的插入,后者代表记录的删除和更新。InnoDB规定两个大类的undo log不能混着存储在同一个页面。
insert类型的undo log事务提交后就没用了,可以直接释放。而update类型的undo log即使事务提交了,也不能立即释放,因为还要服务于MVCC。处理方式不同,自然分开存储要好一些。
- TRX_UNDO_PAGE_START:第一条undo log在页面中的偏移量。
- TRX_UNDO_PAGE_FREE:最后一条undo log结束时偏移量
- TRX_UNDO_PAGE_NODE:List Node节点,用于将undo log页串联起来。
3. Undo log页链表
每对一条记录进行一次修改,都会对应1到2条undo log,一个事务可能会修改很多记录,也就会生成大量的undo log,如果一个undo log页装不下,就必须申请多个undo log页面,这些页面会串联成一条双向链表。
链表头节点页面被称为first undo page
,其它页面称为normal undo page
,它俩的区别是,first undo page
除了存储undo log,还需要存储一些管理信息。
因为不同大类的undo log不能混着存储,所以一个事务必须有两条Undo log链表:insert undo链表和update undo链表。InnoDB又规定,对更新普通表产生的undo log与更新临时表产生的undo log也要分开存储,所以一个事务最多会分配4条undo log链表。
Undo log链表的分配策略是「按需分配」,为了节省资源,只有在用到时才会分配。如果一个事务没有对任何记录做修改,那么就不会分配Undo log链表。
4. Undo log写入
undo log是如何存储的?
4.1 Undo Log Segment Header
InnoDB规定每个 Undo页面链表都对应一个段 ,称之为Undo Log Segment
,链表中的页面都是从段中申请的,所以链表里的first undo page存储了Undo Log Segment Header
部分:
- TRX_UNDO_STATE:链表状态。
- TRX_UNDO_ACTIVE:活跃,有事务正在往里写undo log。
- TRX_UNDO_CACHED:被缓存,链表等待被重用。
- TRX_UNDO_TO_FREE:空闲,insert undo log事务提交后就处于这种状态。
- TRX_UNDO_TO_PURGE:update undo log事务提交后不能被重用时处于这种状态。
- TRX_UNDO_PREPARED:包含处于PREPARE阶段的事务产生的 undo log。
- TRX_UNDO_LAST_LOG:链表中最后一个
Undo Log Header
的位置。 - TRX_UNDO_FSEG_HEADER:链表所属段的
Segment Header
信息。 - TRX_UNDO_PAGE_LIST:链表基节点。
4.2 Undo Log Header
事务往Undo log页面写日志的过程就是直接往里追加,页面写完了就再申请一个新的页面继续写。InnoDB规定,一个事务向一条Undo log链表写入的日志为一组,由于Undo log链表会被重用,所以可能存在一条链表里有多组Undo log,为了区隔开,InnoDB规定事务在写入一组undo log前,必须先写入一个Undo Log Header
:
- TRX_UNDO_TRX_ID:生成本组undo log的事务id。
- TRX_UNDO_TRX_NO:事务提交后的序号。
- TRX_UNDO_DEL_MARKS:是否包含由于Delete mark 操作产生的undo log。
- TRX_UNDO_LOG_START:第一条undo log的起始偏移量。
- TRX_UNDO_XID_EXISTS:是否包含XID信息。
- TRX_UNDO_DICT_TRANS:是否由DDL语句产生。
- TRX_UNDO_TABLE_ID:DDL对应的表id。
- TRX_UNDO_NEXT_LOG:下一组undo log的偏移量。
- TRX_UNDO_PREV_LOG:上一组undo log的偏移量。
- TRX_UNDO_HISTORY_NODE:History链表基节点。
综上所述,一条完整的的Undo log链表应该长这样:
4.3 重用Undo log页
一个事务只要修改了数据,最少会分配1条Undo log链表,每条链表最少包含一个Undo log页面,实际上大量的小事务仅仅修改了很少的数据,每开启一个事务就分配一条链表实在是有点浪费,所以InnoDB会尝试重用Undo log链表。
InnoDB规定,一条Undo log链表可以被重用,必须满足2个条件:
- 链表中只包含一个Undo log页面。
如果链表有很多页面,那么重用的事务即使只写入少量日志,也得维护这些页面,这带来了另一种浪费。
- 页面使用的空间小于3/4。
如果页面剩余空间不多,那么即使重用也意义不大。
对于insert undo链表和update undo链表,两者重用的策略也是不一样的。
- insert undo log只要事务被提交,undo log就没用了,因此insert undo链表重用时可以直接重头开始写,把旧的undo log直接覆盖掉。
- 而对于update undo链表,由于还需要服务于MVCC,因此不能直接覆盖,而是追加写入,这就会导致一个Undo log页面包含多组undo log。
5. 回滚段
一个事务最多分配4条Undo log链表,同一时刻可能有大量事务在并发执行,也就是会存在大量的Undo log链表,为了更好的管理这些链表,InnoDB设计了一个叫Rollback Segment Header
的页面,这个页面有1024个Undo slot,用来存放每条Undo log链表的first undo page的页号。每个Rollback Segment Header
页面都对应一个段,称为「回滚段」。
- TRX_RSEG_MAX_SIZE:回滚段管理的最大Undo log页数量,默认
0xFFFFFFFE
。 - TRX_RSEG_HISTORY_SIZE:History链表占用的页面数量。
- TRX_RSEG_HISTORY:History链表基节点。
- TRX_RSEG_FSEG_HEADER:回滚段对应的
Segment Header
。 - TRX_RSEG_UNDO_SLOTS:1024个undo slot。
5.1 申请Undo log链表
一开始,回滚段中的1024个Undo slot都没有被分配,此时Undo slot被设置成一个特殊值FIL_NULL
,十六进制是0xFFFFFFFF
,代表Undo slot不指向任何Undo log页面。
此时开启一个事务更新数据,需要分配Undo log链表,从回滚段的第1个Undo slot开始遍历,如果不是FIL_NULL
,则分配给当前事务,反之代表已经被其它事务占用,则往后继续寻找。
将Undo slot分配给当前事务后,需要在表空间新建一个Undo Log Segment
,然后从中申请一个Undo log页面作为链表的first undo page,将该页面的页号写入到Undo slot,就算分配完成了。
如果回滚段里1024个Undo slot都名花有主了,MySQL就会报错:
Too many active concurrent transactions
事务提交后,Undo slot的处理方式:
- 如果Undo slot指向的链表可以被重用,Undo slot会被加入到cached链表中,不同大类的Undo slot会被加入到不同的cached链表。一个回滚段会有两条cached链表,分别是insert undo cached链表和update undo cached链表。新事务分配Undo slot,优先从对应的cached链表中分配。
- 如果Undo slot指向的链表不可以被重用
- 如果指向的是insert undo链表,则链表的
TRX_UNDO_STATE
属性会被设为TRX_UNDO_TO_FREE
,之后链表对应的段会被释放掉,Undo slot会被设为FIL_NULL
。 - 如果指向的是update undo链表,则链表的
TRX_UNDO_STATE
属性会被设为TRX_UNDO_TO_PRUGE
,并将Undo slot设为FIL_NULL
,然后将本组undo log写入History链表。
- 如果指向的是insert undo链表,则链表的
5.2 多个回滚段
一个事务最多分配4条Undo log链表,一个回滚段只有1024个Undo slot,也就是说一个回滚段支持的并发事务数的范围是256~1024
,这个数量未免有点太小了,所以InnoDB最多支持配置128个回滚段,也就是最多131072个Undo slot,最少支持32768个并发事务,是完全够用的。
128个回滚段,意味着128个Rollback Segment Header
页面,为了管理这些页面,InnoDB在系统表空间的第5号页面使用了128个8字节大小的格子来存储这写页面的Space ID和页号。
5.3 回滚段分类
这128个回滚段分为两大类:
- 第0号、33~127号属于一类,用于存放更新普通表时所产生的undo log。
- 第1~32号属于一类,用于存放更新临时表时所产生的undo log。
为什么普通表和临时表产生的undo log要分开存储?
Undo log页面也是一个普普通通的页面,在对Undo log页写入数据时,也要记录对应的redo log,用于系统崩溃时的数据恢复。而对于临时表的更新,只在系统运行时有效,崩溃恢复是不用恢复临时表的,也就是说针对临时表的Undo log页的变更,是不用记录redo log的,于是InnoDB才通过不同的回滚段来区分。
6. Undo log恢复
事务执行过程中会不断写入redo log,用于系统崩溃时恢复数据。但是,如果事务执行到一半发生崩溃,且redo log已经刷盘了,那么MySQL重启后还是会根据redo log将数据恢复到事务执行一半的状态,这违背了事务的原子性。此时,必须将这个执行到一半的事务给回滚掉,这个工作就落到了undo log的头上。
MySQL重启后,会加载系统表空间第5号页面,定位到128个回滚段,检查每一个回滚段里的Undo slot对应的Undo log链表的状态,如果状态是TRX_UNDO_ACTIVE
就代表崩溃前有活跃的事务在向链表写入undo log,MySQL会在Undo Segment Header
中通过TRX_UNDO_LAST_LOG
属性找到最后一个Undo Log Header
,里面有事务id以及一些其它信息,再通过undo log将该事务回滚掉。
7. 配置
- 回滚段
启动参数innodb_rollback_segments
用来配置回滚段的数量,可选范围是1~128
,该配置不会影响临时表的回滚段数量始终是32。
- undo表空间
默认情况下,普通表的回滚段都会分配到系统表空间,其实第33~127号回滚段是支持配置到自定义的undo表空间的,但是只能在系统初始化时配置,后续不再支持修改。
innodb_undo_directory
配置指定了undo表空间目录,innodb_undo_tablespaces
配置指定了undo表空间的数量,默认是0。
设立undo表空间的一个好处就是在undo表空间中的文件大到一定程度时,可以自动的将该undo表空间截断成一个小文件。而系统表空间的大小只能不断的增大,却不能截断。