前面学习了redo日志,redo日志保证的是崩溃时事务持久性。我们可以从redo日志恢复到系统崩溃以前。
undo日志就是为了保证事务回滚时事务所作所为都能回到事务执行前。保证了事务的原子性。redo把我们做增删改之前的状态记录下来,帮助MySQL回滚到事务执行之前的样子。
这篇文章了解一下事务ID和undo日志产生就OK了,对于Undo日志存储可以直接略过。
事务两种类型:只读事务,读写事务。
针对于只读事务,MySQL会在其对用户创建的临时表进行增删改的时候才会为其分配事务ID,否则不分配。
这里的临时表指的是create temporary 表名,和我们使用explain SQL的时候在extra上显示的using temporary不一样。前者是用户创建的用户临时表,只针对于当前session有效,后者是MySQL内部临时表。
而针对于读写事务来说,MySQL会在事务执行对某个表进行增删改的时候为其分配一个事务ID,否则不分配。
在系统启动时,系统维护一个全局变量,我们首先从内存中找到MAX TRX ID这个值然后加上256,赋给这个全局变量。
每次出现上述情况的事务时会为其分配一个ID,然后变量进行+1操作。
然后每每这个全局变量是256的倍数时候就会对这个变量进行同步修改到系统表空间中的MAX TRX ID属性处。
然后我们为什么在取值的时候要加上256呢?主要是因为我们系统关闭是可能已经大于当前MAX TRX ID但是还没有到256的倍数,所以我们只要将其+256,就会得到一个唯一的事务ID咯。
我们在介绍数据行的时候就已经提到数据行的三个隐藏列
为了保证原子性,所以Innodb对每个增删改都会在改之间进行一次undo日志的记录。
下面就来介绍一下增、删、改也就是insert、delete、update会产生的undo日志类型以及其细节吧。
我们使用insert的SQL语句时,就会产生一个TRX_UNDO_INSERT_REC类型的undo日志。
所以innodb回滚会发生什么?首先呢,我们插入可能是悲观插入和乐观插入,悲观就是页满了得进行分裂,乐观就是没满直接将数据行插入。但是呢?innodb做的还是得到插入的主键值,然后删除主键值对应的聚簇索引和二级索引。
当我们执行insert插入时,插入的数据行中的隐藏列,我们关心的主要是事务ID和roll_pointer这两个属性。
上图我们在一个事务中先后执行了2次插入操作,它的roll_pointer都指向了对应生成的undo日志。后续将介绍存储undo的页面。
delete操作就稍微有点特殊了。
我们在前面介绍页面的时候介绍了一个数据行的next_record属性,就是指向下一个数据行的指针。我们也说过这些被删除的数据行是可以被重用的,它其实是被一个存储在PAGE_HEADER中的一个PAGE_FREE的属性给链起来了,也就是说被删除的垃圾页面会形成一个链表。
我们在介绍数据行的时候也介绍了一个delete_mask位,用来标记数据行是否被删除。
在事务中呢,删除分为了两个阶段
之所以有这个垃圾链表,就是为了让接下来插入的数据行用来重用的,如果不能用来重用,这个垃圾链表简直没有意义。
同时呢我们需要注意到,新删除的记录是放在垃圾链表的第一个的,可能是只有PAGE_FREE头部地址是被记录的,所以我们只能用头插法插入到链表中。
重用流程:
首先数据行要插入到页中,先检查垃圾链表头的大小是不是>=插入的数据行的大小,如果是就直接将垃圾链表头拉出来覆盖重用。否则就重新创建。
是没有错,它只检查垃圾链表头,其他的都不检查,而且容易出现碎片,因为是大于等于的条件进行重用。
当出现插入新记录时空间不足,检查垃圾链表的空间和碎片的空间够不够放下记录,够就会进行复制页面有用的数据,这是无可奈何之举,这样就可以去掉碎片和不可用的垃圾链表。
我们在改变中间状态时,会生成一个TRX_UNDO_DEL_MARK_REC类型的undo日志。
我们需要注意的是它将旧的事务ID和roll_pointer记录下来了,以及主键和索引列的信息。
所以会出现下图的情况,出现了一个版本链,原本是指向了插入的undo日志,然后删除的中间状态下,删除的undo日志还会指向插入的undo日志。
具体的删除undo日志就是如下
值得注意的就是索引列的各列信息<pos,len,value>,pos指向的是索引列在第几个列。
update有两种情况
如果更新的不是主键列。
- 旧记录 ("a","15")
- 新纪录 ("b","20") 大小没有发生改变
- 新纪录 ("aa0","20") 第一个列大小发生了改变
不更新主键的情况下会插入的undo日志如下:
如果更新的是主键的话,我们的操作将完全不一样。
更新主键的情况下会产生2条undo日志。
为什么要这样呢?主要是因为如果我们在更新的时候,并发的事务要读取这条旧的数据,为了防止脏读,就需要将其置为中间状态,让其他事务同样也可以读到这条旧的数据。如果我们直接删除了其他事务就读不到了,不就脏读了嘛。
接下来就是介绍undo日志会存放在哪里了。。十分的枯燥,而且就到处引来引去。而且感觉讲得云里雾里
这是链表中每个节点的结构,前面的页号+页面偏移量和后面的页号+页面偏移量
为了方便管理链表,就有了链表的基节点,指向首节点和尾节点,以及记录链表的长度。
链表本链在此。
我们前面在图片中出现了一次这个undo日志的存储页面。
我们接下来介绍一下这个undo日志的通用页面。File Header 和 File Trailer就不用介绍了吧,老演员了,和前面介绍的页面是一样的。
我们在上面提到了增删改会产生的undo日志,其中呢,我们可以分成两个种类。
- TRX_UNDO_INSERT : 类型为TRX_UNDO_INSERT_REC的undo日志,一般在insert语句产生,还有就是update更新主键的时候
- TRX_UNDO_UPDATE : 除了TRX_UNDO_INSERT_REC类型的undo日志,其他类型都这个种类的。
当一个事务中,我们生成的undo日志过多了,一个页面肯定是放不下,我们就会创建多个页面进行存储,然后我们使用链表将其链起来。
我们将undo页面链表的第一个页面叫做first undo page ,其他页面叫做normal undo page。
我们上面也提过的,就是我们可以将undo日志分成两个大类,一个是TRX_UNDO_INSERT 和TRX_UNDO_UPDATE ,不同种类的undo日志会存储到不同的undo页面中。所以呢,我们会为普通表和临时表各自维护这两个大类的页面。
但是呢,这个undo链表只有当前事务执行SQL创建了相应种类的undo日志我们才会去创建这个链表,并不是一开始就创建。
对于不同事务来说,不同事务有不同的undo页面链表。
我们前面讲过在B+树的根节点存储了Segment Header 结构,就是存储INODE ENTRY的位置。
我们在上面提到的undo页面链表的结构,第一个undo页面我们叫做first undo page。
因为first undo page有点不一样,它被设计存储了一个Undo Log Segment Header 的部分,用来表示对应段的Segment Header信息以及关于段的其他信息。
说实话, 没懂这个结构有个dio用,就TRX_UNDO_LAST_LOG 在后面有点用,其他的都是一句就过了。
我们再来看看Undo Log Segment Header 里面存放了什么。
好乱真的无语,作者写的这章我真😵了,我看了3遍都还不懂。上面那个结构完全不知道要干什么。
在Undo Log Segment Header下面存储了Undo Log Header块
重用的条件
insert undo链表的重用,对于这个链表当满足上述两个条件时,会直接覆盖重用undo页面。因为插入旧的undo日志在事务提交后旧没有用了。
update undo链表的重用,对于这个链表满足上述两个条件时,会在旧的undo页面中的free处继续写。因为MVCC需要用到旧的undo日志的,是不能覆盖的。
相当于将每个事务维护的first undo page给集合起来,放到一个页面中,这个页面就叫回滚段。
undo slot 中的TRX_RSEG_UNDO_SLOTS 每4字节都是一个默认值FIL_NULL。
当事务需要创建一个undo链表,就会向回滚段申请一个undo slot。 回滚段就会顺序往下找,找到值为FIL_NULL的4字节地址,然后undo链表申请了第一个页面的地址放入到undo slot 中,就是将其地址改变为申请的页面的地址。
当事务提交时,我们需要判断这个slot是不是能被重用:
就是当事务多起来了,每个事务都由4个链表要维护,就要4个undo slot ,一个回滚段就不够,旧版本就一个回滚段。
所以就在系统表空间中申请了128个8字节的格子。
因为这个space ID就指表空间ID,Page Number就是对应表空间的页号。这个表空间ID就表示不同的回滚段可能位于不同的表空间中。这样我们就有128*1024个undo slot 可以分配使用了。
第0号,第33-127号回滚段属于第一类。第0号一定在系统表空间中,他们是针对普通表的undo slot 进行分配。
第1-32号回滚段属于第二类。是分配给临时表的undo slot。无论我们怎么修改回滚段的大小,这32个是一定在的。
在修改针对普通表的回滚段时,对于页面的修改是需要redo日志来记录的,而对于临时表的修改是不需要写redo日志的。
首先向系统表空间申请一个回滚段
回滚段是循环使用的,就是从0 、33-127这几个位置,就循环分配给事务,防止都分配一个回滚段炸了。
先从cache链表中找到对应类型的有没有可以重用的,有就直接将重用链表拿出来重用。没有下一步。
找到一个没被占用的undo slot ,将申请first undo page 位置填到对应的地方。
然后就可以插入undo日志了。
- mysql> show variables like 'innodb_rollback_segments';
- +--------------------------+-------+
- | Variable_name | Value |
- +--------------------------+-------+
- | innodb_rollback_segments | 128 |
- +--------------------------+-------+
- 1 row in set, 1 warning (0.04 sec)
我们可以修改回滚段的大小,但是不管多小临时表的32个回滚段都在的。
后面文章总结MVCC,这算是MVCC的一个前置知识。
如果本文对你有帮助,别忘记给我个3连 ,点赞,转发,评论