前面说了undo日志的文件格式,第一页和后面的页是不同的,填入undo日志之前,会先把undo_page_header属性填满,还有undo_segment_header,undo_log_header。List base node存在undo segment header,list node存在每个undo 页面的undo_page_header。
重用undo页面
前面说了为了提高并发性能,多个事务写入undo日志性能,innoDB每个事务都有单独的undo链表。但这样也会造成内存浪费,针对如果某个事务只执行了很少量的sql,这些undo日志只占用一点点空间,都占用一个事务的链表,是不是太浪费了,于是在事务提交后的某些情况下,重用该事务的undo页面链表。一个链表判断是否可以被重用非常简单:
该链表只包含一个undo页面
如果一个事务执行过程中产生很多的undo日志,那么可能有非常多的页面加入到undo页面链表中。在该事务提交后,如果将整个链表的页面都重用,意味着即是新的事务并没有向undo页面链表写入很多undo日志,那么该链表也得维护非常多的页面,那些用不到的页面也不能被别的事务使用,这样就造成一种浪费。所以innoDB规定,只有在undo链表包含一个undo页面时,该链表才可以被下一个事务使用。
该undo页面已经使用的空间小于整个页面空间的3/4。
Insert undo 和update undo重用的策略也是不同的。
Insert undo链表:
Insert只存储trx_undo_insert_rec的undo日志,这种类型在事务提交后就没用了,就可以清楚掉。所以在某个事务提交后,重用这个事务insert undo链表的时候,可以直接把之前事务写入一组undo日志覆盖掉,从头开始写。
假如有一个事务使用insert undo链表,到事务提交时,只想insert undo链表插入3条undo日志,这个insert undo链表只申请了一个undo页面。假设此刻该页面使用空间小于整个空间的3/4,那么下一个事务就可以重用这个insert undo链表(并且链表满足只有一个页面)。假设此刻有一个新事物重用了该insert undo链表,那么把之前的覆盖掉。
Update undo链表:
Insert undo链表在事务提交后,undo日志可以直接删除,但是update 日志不能再事务提交后直接删除(下一章mvcc会介绍为什么不能被删除)。所以后面的如果想重用,就不是覆盖,而是新加一条数据。
回滚段
我们知道一个事务执行最多分配4个undo页面链表。在同一时刻,不同事务拥有的undo页面链表是不同的,为了管理好这些链表,innoDB设计了rollback segment header页面,在这个页面存放各种链表的第一个页面first undo page的页号,这些页号称为undo slot。可以理解first undo page就是map的key,通过key能找到对应的value,value就是normal undo page。Key会统一管理在rollback segment header里面。
Rollback segment header结构一共16kb:
File header:38个字节。
Trx_rseg_max_Size:4个字节,本rollback segment里管理所有undo链表中undo页面数量之和最大值。换句话说,这里面页面之和不可以超过这个最大值,基本可以默认为无限大。
Trx_rseg_history_Size:4个字节,history链表占用的大小。
Trx_rseg_history:16个字节,history链表的基节点。
Trx_rseg_fseg_header:10个字节,本rollback segment对应10个字节大小的segment header结构,通过他可以找到本段对应的inode entry。
Trx_rseg_undo_slots:4096个字节,存undo链表first undopage 的页号,也就是undo slot集合。(一个页号占用四个字节,对于16kb的页面,这里存着1024个undo slot,所以一共需要1024*4=4096个字节)
File tailder:8个字节。
innoDB规定每个rollback segment header都属于一个段,这个段就是回滚段。与前面介绍段不同的是,这个rollback segment 其实只有一个页面。
从回滚段中申请undo页面链表
初始情况下,我们未向任何一个事务分配undo页面链表,所以对于rollback segment header中的各种undo slot都设置成一个特殊值fil_null(表示该undo slot不指向任何页面)。
随着项目的运行,开始有事务申请链表了,从回滚段的第一个开始看是不是fil_null:
如果是fil_null,那么在表空间创建一个新的段(也就是undo log segment),然后从段里申请一个页面作为undo页面链表的first undo page,然后把该undo slot设置为刚刚申请的这个页面的页号,意味着这个undo slot分配给了这个事务。
如果不是fil_null,说明该undo slot已经指向一个链表,也就是说已经被其他事务占用,于是看看下一个undo slot是否是fil_null。
已知rollback segment header有1024个undo slot,如果1024个值都不为fil_null,意味着都分配给了事务,此时无法在获取新的undo页面链表,则就会回滚这个事务给用户报错:
Too many active concurrent transactions
用户看到这个错误,可以重新执行这个事务(因为可能有其他事务已经提交,该事务就可以分配undo链表)。
当一个事务提交时,他所占用的undo slot有两种命运:
如果该undo slot指向的undo页面链表符合重用(里面只有一个页面,且小于总空间的3/4)
该undo slot就处于被缓存状态,这时候undo链表的trx_undo_state属性会被设置成trx_undo_cache(该属性在first undo page的undo log segment header)。
被缓存的undo slot会加入链表,如果是insert undo会加入insert undo cache链表,如果是update undo会加入update undo cache链表。
一个回滚段就对应着两个cache链表,如果有新事物需要分配undo slot会先去cache链表找,如果没找到,则再去回滚段rollback segment header页面中找。
如果该undo slot指向undo页面链表不符合被重用的条件,那么针对该undo slot对应的undo页面链表类型不同,会有不同的处理方式:
如果对应的是insert undo 链表,则该undo页面链表的trx_undo_state属性会被设置为trx_undo_to_free,之后该undo页面对应的段会被释放,意味着可以挪做他用,然后把undo slot的值设置为fil_null。
如果对应的是update undo链表,则该undo页面链表的trx_undo_state属性会被设置为trx_undo_to_pruge,之后则会吧undo slot的值设置为fil_null,然后把本次事务写入的一组undo日志放入所谓的history链表中。(注意这里不会释放挪做他用)
多个段回滚
前面说了一个事务最多分配四个undo页面链表,而一个回滚段有1024个undo slot,显示然slot数量有点少。也就是一个回滚段最多容纳1024个读写事务同时执行,再多就崩溃了。因为前面说过rollback unde segment段是固定只有一个页的,不可能一直无线增长。
早期确实只有一个回滚段,但随着业务和机器性能的提升,innoDB一口气定义128个回滚段,相当于128*1024 = 131072个undo slot,那么意思就是支持131072个事务同时执行。
每个回滚段都有一个rollback segment header,所以自然有128个rollback segment header,那么这些自然会统一存起来,于是innoDB在系统空间表第5个页面的某个区域包含了128个8个字节大小的格子,每个格子的构造大概就是:
Space id:4个字节大小,代表表空间的id。
Page number:4个子节大小,代表一个页号。
也就是每8个字节代表一个指针,指向rollback segment header,这里需要注意,不同的segment header对应不同的表空间id。
综上我们知道:系统表空间第五号页面存着128个rollback segment header页面地址,就相当于回滚段。每个回滚段里有1024个undo slot,每个undo slot对应一个undo页面链表。
回滚段分类
回滚段从0开始,吧128个编号的话,第一个就是第0号回滚段,最后一个就是127号回滚段。这128个分为两大类。
第0号、第33~127号属于一类,其中第0号必须在系统表空间,第33~127号既可以咋系统表空间,又可以在undo表空间。(如果一个事务在执行过程中由于对普通表记录做了改动需要分配undo页面链表时,必须从这一类段中分配相对应的undo slot)
第1~32号属于一类。这些必须在临时表空间中(对应的数据目录ibtmp1文件)。
如果一个事务对临时表修改了,则必须从这一类中分配相对应的undo slot。
也就是说两类回滚段一个管理者临时表,一个管理着普通表的undo slot。那么为什么要把普通表和临时表的undo slot分开呢?
因为undo页面本身类型就是fil_page_undo_log的页面简称,说到底他也是个普通的页面。我们前面说够,修改页面之前,一定要先把对应的redo日志写上,这样系统崩溃才能恢复数据。我们在写undo日志本身也是个写页面的过程,所以也会有redo日志记录,防止丢失,innoDB还设计了许多redo日志类型,如mlog_undo_hdr_ cache、mlog_undo_insert、mlog_undo_init等等,也就是说我们对undo页面做任何记录的改动都会记录相对应的redo日志。那么对于临时表,并不需要系统崩溃后恢复,所以临时表的undo日志写入时候,并没有记录redo日志。总结,对于undo日志的记录,会记录redo日志防止系统崩溃数据丢失,而临时表本身就是系统关闭所有数据不需要恢复,所以没有记录redo日志。
为事务分配undo页面链表详细过程
接下来我们以事务对普通表做改动为例,给大家梳理一下事务执行过程中分配undo页面链表时的完整过程。
首先事务在执行过程中对普通表做了首次改动,在系统表空间第5号页面中分配回滚段(就是获取一个rollback segment header页面地址)。一旦某个回滚段被分配了这个事务,那么之后该事务对普通表记录做修改,就不会重新分配。
使用传说中的round-robin(循环方式)获取rollback segment header,当第0个回滚段已经分配了事务,则就看第33个回滚段,依次看下一个。
分配了回滚段后,就要看这里面两个cache链表,如果insert就看insert undo cache链表,如果update 就看update undo cache链表里是否存在undo slot,如果有,则吧undo slot分配给该事务。
如果没有缓存undo slot,则就去rollback segment header页面找一个可用的undo slot分配给该事务。(前面也说了如果找undo slot,就是从第0个开始,如果不是ful_null,就依次找到1024个ful_null)
找到undo slot后,如果是缓存中找到的,说明已经分配了undo log segment,如果不是,则需要重新分配undo log segment,然后从undo log segment 申请一个first作为first undo page页面。
然后事务就可以吧undo日志写入undo页面链表里。
临时表对上亦如此,就不赘述。
回滚段相关配置
配置回滚段数量:
前面说过有128个回滚段,这是个默认值,可以通过innoDB_rollback_segments来配置回滚段,可配置1~128。但这个参数不影响临时回滚段,他一直是32。
也就是说如果这个参数设置为1,临时回滚段还是32.
如果这个参数在2~33之间,普通回滚段还是1.
如果这个参数大于33,则普通回滚段的数量是这个参数减去32的剩余数量。
配置undo表空间:
默认情况下,第0号、第33号到底127号都是在系统表空间。其中0号回滚段一直在系统表空间,但33~127号在自定义undo表空间。可以修改配置,但只能在初始化的时候创建,一旦分配好里面有数据就不能修改。
通过innoDB_undo_directory指定undo表空间所在目录,如果没有指定参数,则默认undo表空间所在目录是数据目录。
通过innoDB_undo_tablespace定义undo表空间数量。该参数默认为0,表明不创建任何undo表空间。
第33~127可以平分分不到不同的undo表空间。
比如我们设置innoDB_rollback_segments的参数是35,innodb_undo_tablespaces的为2,则33,和34号回滚段分布到一个undo表空间中。
设计undo表空间的好处是,会吧undo表空间截取truncate成一个个小文件,而系统表空间大小只会越拉越大。