InnoDB之Undo log写入和恢复

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页。
image.png
File Header和File Trailer是通用页结构了,不再赘述,重点关注Undo Page Header:
image.png

  • TRX_UNDO_PAGE_TYPE :页面存储的undo log种类。

undo log分为两大类,分别是TRX_UNDO_INSERTTRX_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页面,这些页面会串联成一条双向链表。
image.png
链表头节点页面被称为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链表。
image.png
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部分:
image.png

  • 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
image.png

  • 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链表应该长这样:
image.png

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。

image.png

5. 回滚段

一个事务最多分配4条Undo log链表,同一时刻可能有大量事务在并发执行,也就是会存在大量的Undo log链表,为了更好的管理这些链表,InnoDB设计了一个叫Rollback Segment Header的页面,这个页面有1024个Undo slot,用来存放每条Undo log链表的first undo page的页号。每个Rollback Segment Header页面都对应一个段,称为「回滚段」。
image.png

  • 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链表。

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和页号。
image.png

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表空间截断成一个小文件。而系统表空间的大小只能不断的增大,却不能截断。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值