我们现在知道一个事务在执行过程中最多可以分配4个 Undo 页面链表,在同一时刻不同事务拥有的 Undo 页面链表是不一样的,所以在同一时刻系统里其实可以有许许多多个 Undo 页面链表存在。为了更好的管理这些链表,设计InnoDB的大佬又设计了一个称之为 Rollback Segment Header 的页面,在这个页面中存放了各个 Undo 页面链表的 first undo page 的页号,他们把这些页号称之为 undo slot 。
设计InnoDB的大佬规定,每一个 Rollback Segment Header 页面都对应着一个段,这个段就称为Rollback Segment,翻译过来就是回滚段。与我们之前介绍的各种段不同的是,这个Rollback Segment里其实只有一个页面(这可能是设计InnoDB的大佬们的一种洁癖,他们可能觉得为了某个目的去分配页面的话都得先申请一个段,或者他们觉得虽然目前版本的MySQL里Rollback Segment里其实只有一个页面,但可能之后的版本里会增加页面也说不定)。
初始情况下,由于未向任何事务分配任何Undo页面链表,所以对于一个Rollback Segment Header页面来说,它的各个undo slot都被设置成了一个特殊的值:FIL_NULL(对应的十六进制就是0xFFFFFFFF),表示该undo slot不指向任何页面。
随着时间的流逝,开始有事务需要分配Undo页面链表了,就从回滚段的第一个undo slot开始,看看该undo slot的值是不是FIL_NULL:
一个Rollback Segment Header页面中包含1024个undo slot,如果这1024个undo slot的值都不为FIL_NULL,这就意味着这1024个undo slot都已经名花有主(被分配给了某个事务),此时由于新事务无法再获得新的Undo页面链表,就会回滚这个事务并且给用户报错:
Too many active concurrent transactions
用户看到这个错误,可以选择重新执行这个事务(可能重新执行时有别的事务提交了,该事务就可以被分配Undo页面链表了)。
当一个事务提交时,它所占用的undo slot有两种命运:
如果该undo slot指向的Undo页面链表符合被重用的条件(就是我们上面说的Undo页面链表只占用一个页面并且已使用空间小于整个页面的3/4)。该 undo slot 就处于被缓存的状态,设计InnoDB的大佬规定这时该Undo页面链表的 TRX_UNDO_STATE 属性(该属性在 first undo page 的 Undo Log Segment Header 部分)会被设置为 TRX_UNDO_CACHED。
被缓存的undo slot都会被加入到一个链表,根据对应的Undo页面链表的类型不同,也会被加入到不同的链表:
如果对应的Undo页面链表是insert undo链表,则该undo slot会被加入insert undo cached链表。
如果对应的Undo页面链表是update undo链表,则该undo slot会被加入update undo cached链表。
一个回滚段就对应着上述两个cached链表,如果有新事务要分配undo slot时,
先从对应的cached链表中找。如果没有被缓存的undo slot,才会到回滚段的 Rollback Segment Header页面中再去找。
如果该undo slot指向的Undo页面链表不符合被重用的条件,那么针对该undo slot对应的Undo页面链表类型不同,也会有不同的处理:和 MVCC 紧密相关
如果对应的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 链表中
(需要注意的是,这里并不会将Undo页面链表对应的段给释放掉,因为这些undo日志还有用呢~)。
我们说一个事务执行过程中最多分配 4 个 Undo 页面链表,而一个回滚段里只有1024个 undo slot,很显然 undo slot的数量有点少啊。我们即使假设一个读写事务执行过程中只分配1个Undo页面链表,那 1024 个 undo slot 也只能支持1024个读写事务同时执行,再多了就崩溃了。这就相当于会议室只能容下1024个班长同时开会,如果有几千人同时到会议室开会的话,那后来的那些班长就没地方坐了,只能等待前面的人开完会自己再进去开。
话说在InnoDB的早期发展阶段的确只有一个回滚段,但是设计InnoDB的大佬后来意识到了这个问题,咋解决这问题呢?会议室不够,多盖几个会议室不就得了。所以设计InnoDB的大佬一口气定义了128个回滚段,也就相当于有了128 × 1024 = 131072 个 undo slot。假设一个读写事务执行过程中只分配1个Undo页面链表,那么就可以同时支持131072个读写事务并发执行(这么多事务在一台机器上并发执行,还真没见过呢~)。
每个回滚段都对应着一个Rollback Segment Header页面,有128个回滚段,自然就要有128个Rollback Segment Header页面,这些页面的地址总得找个地方存一下吧!于是设计InnoDB的大佬在系统表空间的第5号页面的某个区域包含了128个8字节大小的格子:
每个8字节的格子的构造就像这样:
如果所示,每个8字节的格子其实由两部分组成:
4字节大小的Space ID,代表一个表空间的ID。
4字节大小的Page number,代表一个页号。
也就是说每个8字节大小的格子相当于一个指针,指向某个表空间中的某个页面,这些页面就是Rollback Segment Header。
这里需要注意的一点事,要定位一个Rollback Segment Header还需要知道对应的表空间ID,这也就意味着不同的回滚段可能分布在不同的表空间中。
所以通过上面的叙述我们可以大致清楚, 在系统表空间的第5号页面中存储了 128 个 Rollback Segment Header 页面地址,每个 Rollback Segment Header 就相当于一个回滚段。在Rollback Segment Header页面中,又包含1024个undo slot,每个undo slot都对应一个Undo页面链表。我们画个示意图:
我们把这128个回滚段给编一下号,最开始的回滚段称之为第0号回滚段,之后依次递增,最后一个回滚段就称之为第127号回滚段。这128个回滚段可以被分成两大类:
第0号、第33~127号回滚段属于一类。其中第0号回滚段必须在系统表空间中(就是说第0号回滚段对应的Rollback Segment Header页面必须在系统表空间中),第33~127号回滚段既可以在系统表空间中,也可以在自己配置的undo表空间中,关于怎么配置我们稍后再说。如果一个事务在执行过程中由于对普通表的记录做了改动需要分配Undo页面链表时,必须从这一类的段中分配相应的undo slot。
第1~32号回滚段属于一类。这些回滚段必须在临时表空间(对应着数据目录中的ibtmp1文件)中。
如果一个事务在执行过程中由于对临时表的记录做了改动需要分配Undo页面链表时,必须从这一类的段中分配相应的undo slot。也就是说 如果一个事务在执行过程中既对普通表的记录做了改动,又对临时表的记录做了改动,那么需要为这个记录分配2个回滚段,再分别到这两个回滚段中分配对应的undo slot。
在服务器因为崩溃而恢复的过程中, 首先需要按照 redo 日志将各个页面的数据恢复到崩溃之前的状态,这样可以保证已经提交的事务的持久性。但是这里仍然存在一个问题,就是 那些没有提交的事务写的 redo 日志可能也已经刷盘,那么这些未提交的事务修改过的页面在 MySOL 服务器重启时可能也被恢复了。为了保证事务的原子性,有必要在服务器重启时将这些未提交的事务回滚掉。那么,怎么找到这些未提交的事务呢? 这个工作又落到了 undo 日志头上。我们可以通过系统表空间的第5号页面定位到 128 个回滚段的位置,在每一个回滚段的1,024 个 undo slot 中找到那些值不为 FIL NULL 的 undo slot,每一个 undo slot 对应着一个Undo 页面链表。然后从 Undo 页面链表第一个页面的 Undo Log Segment Header 中找到 TRXUNDO STATE 属性,该属性标识当前 Undo 页面链表所处的状态。如果该属性的值为 TRXUNDO ACTIVE,则意味着有一个活跃的事务正在向这个 Undo 页面链表中写入 undo 日志,然后再在 Undo Segment Header 中找到 TRX UNDO LAST LOG 属性,通过该属性可以找到本Undo 页面链表最后一个 Undo Log Header 的位置。从该 Undo Log Header 中可以找到对应事务的事务d 以及一些其他信息,则该事务id 对应的事务就是未提交的事务。通过 undo 日志中记录的信息将该事务对页面所做的更改全部回滚掉,这样就保证了事务的原子性。
redo 日志最终是需要写在磁盘里的,
undo 日志是存在内存里的,但是写 undo 页对应 的 redo 是在磁盘里的