很多关于undo日志的文章都写得很好,但由于undo日志本身原理繁杂,容易混乱,故写此文用于梳理
一、undo日志是干什么的?
用于事务回滚,保证事务的原子性
二、必须要考虑的事:
1.undo日志要记什么内容?
2.undo日志在事务提交前,事务回滚后,事务提交后,状态是怎样的?
3.如何对undo日志进行分类和管理?
首先,来解决第一个问题:记什么?
undo日志实际上应该记一个操作。事务中执行了什么,我就反向做什么。比如事务中插入了一个数据,我就要记住插入数据的id以便反向删除;事务更新了一个数据,我就要记住事务更新前的数据是什么,以便恢复数据。
但是,又有个问题。我怎么知道这条undo日志就是这个事务的呢?
因此,undo日志中,还必须记下trx-id,即事务id
然后,我们还发现,一个事务中可能会对多张表的数据进行增删改。undo日志就只记下一个反向操作所需的数据,但是没告诉我是操作谁啊!
于是,undo日志中,还必须记下table-no,操作表的id
事实上,undo日志中还要记很对其它信息,这里就不一一列举了。
# 直接来看insert类型undo日志的结构:
诶,等等,trx-id去哪了呢?其实我们仔细想想, 不同事务的undo日志可能存放在一起吗?如果可以,岂不是乱套了!那么到底在哪里记录trx-id呢?稍安勿躁!后面自会揭晓。
从上图可以看到,insert类型的undo日志中,【主键各列信息】这一项就是用来删除插入记录的,符合我们开始的预想。
# 再来看看delete类型的undo日志:
嗯!怎么这里又冒出来trx-id了呢?而且打上了一个old的修饰。事实上,这里的old trx_id是为了一个叫MVCC的特性服务的,这里可以暂且不理它。
注意,这个删除日志记录的【删除记录原本的数据】并不是用来在事务回滚时,重新插入旧记录的!你会发现它只是记录了删除记录原本的主键值,并没有把其它字段的值完整记录下来。
事实上,在事务提交之前,你以为你真的把记录删掉了吗?并没有。真正的删除会等到你提交事务后,事务会根据delete的undo日志去执行实际的删除。这样做的好处是显然易见的:减少了delete日志需要记录的内容。
完整的过程是这样的:
①在你未提交事务时,你删除的记录只会打上一个删除标记,但是由于该记录没有移到垃圾链表(别告诉我你连垃圾链表都不知道是什么),该记录仍然可以被其它事务访问,而不可以被其它数据重用。(此外,还会修改old_trx_id和old roll_pointer的值,但这并不是重点)
②如果你提交了事务,那么只需要把这个删除标记改回来就好。
③如果你回滚了事务,那么就要根据undo日志执行实际的删除,这一阶段称之为purge(净化)。
# 最后来看看update类型的undo日志
首先要清楚,在事务提交前,事务的更新就已经完成了。但这一更新过程是有明显差别的。
更新的方式主要分为三类:
对于不更新主键的情况,①如果说修改后每个字段和原来的字段占用空间都一样,就可以“原地更新”,直接覆盖掉旧记录的内容;②如果说修改后又任何一个字段占用空间和原来不同,就不会采用就地更新的方法,而是先将旧记录删除并假如垃圾链表,而后直接申请一块新页面将新数据写入;
不更新主键的情况会产生如下的undo日志:
我们只需要关注两条信息:①被更新列更新前的信息、②凡是被索引的列的各列信息
在事务回滚后,根据②找到修改前数据的位置,根据①将数据改回修改前的样子
【仔细观察会发现,上图的old roll-pointer指向了一条undo日志。这其实就是版本链,但这不是重点,只需有个印象即可】
【tips:一条记录删除并移入垃圾链表,此时它就是垃圾链表的头节点。新插入记录不会直接在页面空闲区域插入,而是会先检查垃圾链表的头节点。如果头节点的记录空间比自己要插入记录空间大,那么这块区域就会被重用;否则,再插入到空闲区域】
对于更新主键的情况来说,由于更新主键后,有可能由于主键值和原来差得太对,记录都不在原来的页面了,因此对比【不更新主键②】来说,在删除旧记录后,新纪录的插入就需要依靠聚簇索引来定位应该插入到的页面。
更新主键的情况会生成两条undo日志,一条delete类型的undo日志,一条insert类型的undo日志。
总结一下:
接着,我们来探讨如何对undo日志进行分类。
上面其实我们已经将undo日志的页面做了分类了。但仅仅将undo日志页面分类,就够了吗?显然不够。
比如,一个insert类型的undo页面就能将所有insert类型的undo日志记录完吗?肯定不行嘛!那就会有很多insert类型的undo页面,这些页面是不是应该串成链表?
在MySQL中,链表结构总是用来进行分类。每一个链表节点都有这样相似的结构:
简而言之,就是有一个双向指针。
而链表基节点(就是用来管理链表结构的)结构如图:
最左侧的就是链表基节点
我们可以将insert类型,detete类型和update类型的undo页面串成链表。
但这还不够。
我们是根据事务进行数据回滚的,自然而然就应该以事务id为标准进行分类啊!
因此,链表的分配是这样的:一个事务执行时,按需分配。比如事务中执行了一个insert操作,那么就会分配一个insert undo页面链表。如果执行了一个update主键的操作,就会分配一个insert undo页面链表以及一个delete undo页面链表。
既然链表是按事务进行管理的,自然就应该在每个链表的头节点处记录下它的事务id。事实上,这个信息就记录在链表头节点的Undo Log Header里面。
在Undo Log Header里头还有一个标记事务内部子事件的执行顺序。为什么要记录这个顺序呢?这是因为事务执行是有序的,我们在回滚时应该就按照这个顺序的相反方向进行回滚。
总的来说,Undo Log Header就是用来管理链表事务的。【一组】=【一个事务 】
链表头节点中还有一个Undo log segment header,里面的segment Header是指向段属性的指针。段是什么?你不会不知道吧?简而言之就是一些为了同一个目而存在的一片连续的页。段能够很好的管理这些连续的页。每个链表都会对应一个段,链表中的页面都是从这个段中申请的。
【其实,段是一些零散的页和完整的区的集合。但这里为了方便理解,就这么说了。】
# undo日志是怎么组织存储的?
上面说了一堆不同事务的链表,它们又是怎么组织存储的呢?
直接来看:
好家伙,那么抽象!我们从下往上看。
我们做一个想象:undo链表就像一个班级,而undo链表的头节点就是班长。通过班长就能很快的找到对应的班级。这里undo slot就是undo链表头节点的页号。哦!原来这一个个undo slot就是“班长”啊!班长那么多,怎么集中起来?当然是通过会议室--rollback segment header(回滚段)来集中啦!会议室装不下那么多“班长”在那么办?再多建几个“会议室”呗!多个“会议室”的地址就放在了系统表空间的第五号页面中。
我们怎样为一个事务分配对应的链表呢?先卖个关子,我们先来想想,事务提交后,undo日志还有用吗?
我们说,undo日志是用来在事务回滚后进行数据恢复的。事务提交了,自然就没有回滚的需求了,理应删除掉这个undo日志啊!事实上,由于MVCC特性(😡😡怎么又是他),只有insert类型的undo日志会在事务提交后释放掉,而update,deletr类型的undo类型日志不会被删掉,而是加入到版本链中,接着为MVCC服务。
我们来看看undo日志的释放过程:
【重用指的是对undo链表的重新利用,如果一个undo页面链表只有一个页面(链表头节点)且页面占用空间<75%,那么这个链表就会被重用。】
我猜你肯定还有点疑惑,再来看我们怎样为一个事务分配对应的链表:
看不懂就多看几遍上面两张图,不多解释。
🤔🤔好像还有一些细节,太多了 ,就不多一一列举了。核心内容就这些!