栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Undo日志

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Undo日志

提示:文章内容来自《mysql是怎样运行的》以及部分B站宋红康老师的视频,这里仅仅是我的笔记,对重点内容的记录。强烈推荐购买这本书《mysql是怎样运行的》。

文章目录
  • 一、事务id
  • 三、Delete操作对应的undo日志
    •        综上,在执行一条删除语句的过程中,在删除语句所在的事务提交之前,只会把delete_flag 设置为1,这个undo日志被TRX_UNDO_DEL_MARK_REC类型的undo日志记录
  • 四、Update操作对应的undo日志
  • 五、增删改对二级索引的影响
  • 六、段:
  • 七、Undo Log Header
  • 八、重用undo日志
  • 九、回滚段:
  • 十、为事务分配undo页面链表的详细过程
  • 十一、Undo log在崩溃恢复时的作用


我们说过,事务需要保证原子性,就是事务中的操作,要么全部完成,要么什么也不做,但偏偏有的时候,事务在执行一半的时候会出现问题
比如服务器本身的错误、操作系统错误、断电
程序员可以在事务执行的过程中,输入rollback语句结束当前事务的执行
这两种情况都会导致事务在执行一半的时候终止,但是事务在执行的过程中,可能已经修改了很多东西,为了保证原子性,我们需要改回原来的的样子,这个过程叫做回滚,回滚需要记录一些日志,称为undo日志。Undo日志只记录写操作的日志,读操作select是不会记录的。

一、事务id

1、前面我们说过可以通过start transaction read only 语句开启只读事务,只读事务对普通的不能进行增删改,但是可以对临时表增删改(因为临时表只对当前会话可见,其他事务见不到),对于只读事务,只有在它第一次对临时表写时,才会为这个事务分配一个事务id
对于读写事务(start transaction read write),只有在它第一次对某个表进行写时,才会分配一个事务id

2、前面我们再说innodb的行格式的时候,强调过在聚簇索引中,除了会保存完整的用户记录外,还有三个隐藏列
Row_id、trx_id、roll_pointer,其中row_id是在用户没有创建主键或者没有创建非空且唯一的列的时候加上的,trx_id就是对聚簇索引记录进行写操作时的事务对应的事务id ,roll_pointer后面说

二、undo日志的格式
为了实现事务的原子性,Innodb存储引擎在实际进行记录的增删改时,都需要先把对应的undo日志记录下来,一般每对一条记录进行一次改动,就会对应一条undo日志,但是在某些更新记录的操作中,也可能对应着两条undo日志,一个事务在执行过程中可能新增、删除、修改若干条记录,因此需要记录很多undo日志,这些日志会从0开始编号,也就是说根据生成的顺序分别为第0号日志。。。。这个编号也称为undo no
这些undo日志会被记录到FIL_PAGE_UNDO_LOG页面中

Insert操作对应的undo日志
当向一个表插入记录时,有乐观插入和悲观插入的区分,但是无论怎么插入,最终导致的结果就是这条记录被放进了数据页。如果希望回滚这个插入操作,就直接把这条记录删除就好了,也就是说在写对应的undo日志时,只要把这条记录的主键信息记上就好了。因此innodb引入了一个类型为TRX_UNDO_INSERT_REC的undo日志
它的结构如下

Undo no 在一个事务中是从0开始递增的,也就是说,只要事务没有提交,每生成一条undo日志,那么该日志的undo no就增1
如果记录中的主键只包含一列,那么在类型为TRX_UNDO_INSERT_REC的undo日志中,只需要把该列占用的存储空间和真实值记录下来。如果主键中包含多个列,那么每个列占用的存储空间的大小和真实值都要记录下来
注意:当我们向某个表中插入一条数据的时候,实际上需要向聚簇索引和所有的二级索引中都插入一条记录。不过在记录undo日志的时候,我们只需要针对聚簇索引的记录来记录一条undo日志就好了。聚簇索引记录和二级索引记录是一一对应的,我们在回滚insert操作时,只需要知道该记录的主键信息,然后根据主键信息进行相应的删除操作。在执行删除操作时,就把聚簇索引和所有的二级索引中的记录都删除了,后面说的delete和update也是一样的

roll_pointer实际就是一个指针,指向记录对应的undo日志

三、Delete操作对应的undo日志

我们知道,一个页中的记录是通过next_record属性组成的单向链表,我们可以把这个称为正常记录的链表。前面也说过,被删除的记录也会通过这个属性形成一个链表,也就是垃圾链表。页中的Page Header中有一个名为PAGE_FREE的属性,它指向由垃圾链表的头部。

假设现在准备把正常记录的最后一条删除
那么这个删除过程包括两个阶段:
阶段1:仅仅将记录的delete_flag设置为1,其他不做修改,并不把这条记录放到垃圾链表中,在删除语句所在的事务提交前,被删除的记录一致处于这种中间状态(为MVCC做铺垫)这个阶段称之为 delete mark 。

       可以看到, 正常记录链表 中的最后一条记录的 delete_mask 值被设置为 1 ,但是并没有被加入到 垃圾链表 。也就是此时记录处于一个 中间状态

阶段2:当该删除语句所在的事务提交后,会有专门的线程把真正的记录删除掉,其实就是把这个记录从正常的记录中移除,并且加入到垃圾链中。这个过程也称为purge。
在2完成后,这个记录才算真正的删除了,该记录所占用的存储空间才能被真正的利用。

注意:在将被删除的记录加到垃圾链表中,实际上是加到链表的头节点的位置,还会跟着修改PAGE_FREE的值


       特别的:页面的Page Header中有一个名为PAGE_GARBAGE的属性,该属性记录着当前页面中可重用的存储空间占用的字节数,每当有新删除的记录加到链表后,都会把这个PAGE_GARBAGE的值加上已删除的记录占用的存储空间的大小
PAGE_FREE指向垃圾链表的头节点,之后每当新插入记录的时候,首先判断垃圾链表的头节点代表的已删除的记录所占用的存储空间是否能够容纳这条新插入的记录。如果无法容纳,就直接向页面申请新的存储空间来容纳这条新插入的记录,如果可以容纳,就直接重用这条已删除记录的存储空间,并让PAGE_FREE指向下一条已删除记录
这里有一个问题:如果新插入的那条记录占用的存储空间,小于垃圾链表头节点对应的已删除记录占用的存储空间,那就意味着头节点对应的记录所占用的存储空间中,有一部分空间用不到,这部分空间就算是一个碎片空间,随着新纪录越插越多,由此产生的碎片空间也可能越来越多,这些空间岂不是用不到?也不是这些碎片空间占用的存储空间大小会被统计到PAGE_GABAGE属性中,这些碎片空间在整个页面快使用完前不会被重新利用。不过当页面快满时,如果插入一条新的记录,此时页面中不能分配一条完整的记录的时候,会先看PAGE_GARBAGE的空间和剩余的可利用的空间相加之后是否可以容纳这条记录,如果可以,InnoDB会尝试重新组织页内的记录,重新组织的过程就是先开辟一个临时页面,把页面内的记录一次插入一遍,因为依次插入记录时,不会产生碎片,之后再把临时页面的内容复制到本页面,这样就可以把那些碎片空间解放出来了(很显然,重新组织页面会比较费时,但是这也体现了mysql在尽力充分的利用空间)


       综上,在执行一条删除语句的过程中,在删除语句所在的事务提交之前,只会把delete_flag 设置为1,这个undo日志被TRX_UNDO_DEL_MARK_REC类型的undo日志记录 四、Update操作对应的undo日志

①不更新主键

就地更新

       在更新记录时,对于被更新的每个列来说,如果更新后的列与更新前的列占用的存储空间一样大,那么可以进行就地更新,也就是直接在原纪录的基础上修改对应的列的值。再强调一遍,是每个列的更新前后占用的存储空间一样大,只要有任何一个被更新的列在更新前比更新后占用的存储空前大,或者在更新前比更新后占用的存储空间小,就不能进行就地更新。

先删除旧纪录,再插入新纪录

        在不更新主键的情况下,如果有任何一个被更新的列在更新前和更新后占用的存储空间的大小不一致,那么就需要先把这条记录从聚簇索引中删除,然后在根据更新后的值创建一条新的记录插入到页面中。
        注意,这里说的删除,不是delete mark操作,而是真正的删除掉,也就是把这条记录从正常记录中移除,并加入到垃圾链中
        如果先创建的记录占用的存储空间不超过旧纪录占用的存储空间,那么可以直接重用加到垃圾链中的旧纪录所占用的存储空间,否则需要在页面中申请一块空间供新纪录使用,如果没有可用的空间,就得进行页分裂,然后插入新的记录。Undo日志由TRX_UNDO_UPD_EXIST_REC记录
②更新主键
步骤1:将旧纪录进行delete mark 操作
        高能注意:这里是delete mark操作,也就是说,在update语句中所在的事务提交之前,对旧纪录只做一个delete mark操作,在事务提交后才有专门的线程执行purge操,从而把它加入到垃圾链表中。
        之所以只对旧纪录执行delete mark操作,是因为别的事务也可能同时访问这条记录,如果把它真正的删除并加入垃圾链表中,别的事务就访问不到了,这个就是MVCC。
步骤2:根据更新后各列的值创建一条新纪录,并将其插入到聚簇索引中。
       针对update语句更新主键的情况,在对该记录进行delete mark操作时,会记录一条类型为TRX_UNDO_DEL_MARK_REC日志;之后插入新纪录时,会记录一条类型为TRX_UNDO_INSERT_REC的undo日志。也就是说,每一条记录的主键值进行改动,都会记录两条undo日志。

五、增删改对二级索引的影响

       Insert和delete差不多,但是update稍微有些不同,如果我们的update中没有涉及二级索引的列,那么就不需要对二级索引执行任何操作
相反地
        对旧的二级索引记录执行delete mark
        根据更细后的值创建新的二级索引记录,然后在二级索引对应的B+树中,重新定位到它的位置并插入
        另外需要强调是,虽然只有聚簇索引记录才会有trx_id、roll_pointer这些属性,不过每当我们增删改一条二级索引记录的时候,都会影响这条二级索引所在页面的Page Header部分中的一个名为PAGE_MAX_TRX_ID的属性

        前面只要说了为什么需要undo日志,以及insert 、update、delete这些改动的数据的语句都会产生什么类型的undo日志,下面就说说undo日志会被写到什么地方,以及写入的过程需要注意什么
        写入undo日志的过程会用到很多链表,这些链表都有相同的节点结构:

        为了更好的管理链表,还提出了基节点的结构,这个结构里存储了链表的头节点、尾节点、以及链表的长度信息

FIL_PAGE_UNDO_LOG:
这个类型的页是专门用来存储undo日志的

Undo page header的结构:

TRX_UNDO_PAGE_TYPE:本页面准备存储什么类型的日志
把undo日志分为两大类
①TRX_UNDO_INSERT:类型为TRX_UNDO_INSERT_REC的undo日志属于这个大类,一般由insert语句产生;当update语句中有更新主键的情况也会产生此类型的日志;我们把这个大类的日志称为insert undo 日志
②TRX_UNDO_UPDATE:一般由delete和update语句产生的日志。

Undo页面链表:
同一个undo页面要么只存储TRX_UNDO_INSERT大类的undo日志,要么只存储TRX_UNDO_UPDATE大类的undo日志,不能混着
这俩还不能在同一个链表中
一个事务最多分配4个链表
普通表的insert undo链表
普通表的update undo链表
临时表的insert undo链表
临时表的update undo链表

六、段:

段本质上是由若干个零散的页面和若干个完整的区组成
每个undo页面都对应一个着一个段,称为undo log segment,也就说,链表中的页都是从这个段中申请的,在undo页面链表的第一个页面中设计了一个名为undo log sengment header的部分

TRX_UNDO_STATE :本undo页面链表处于什么状态,
①TRX_UNDO_ACTIVE:活跃状态,也就是一个活跃的事务正在向这个Undo页面链表中写入undo日志
②TRX_UNDO_CACHED:被缓存的状态,处于该状态的undo页面链表等待之后被其他事务重用
③TRX_UNDO_TO_FREE:等待被释放的状态,对于insert undo链表来说,如果它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态
④TRX_UNDO_TO_PURGE:等待被purge的状态,对于update undo链表来说,如果在它对应的事务提交之后,该链表不能被重用,就会处于这种状态
⑤TRX_UNDO_PREPARED:处于此状态的undo页面列表用于存储处于OREPARE阶段的事务产生的日志(分布式事务)
TRX_UNDO_LAST_LOG:本undo页面列表中最后一个undo log header的位置
TRX_UNDO_FSEG_HEADER:本undo页面列表对应段的segment header信息
TRX_UNDO_PAGE_LIST:undo页面链表的基节点

七、Undo Log Header

一个事务在向Undo页面中写入undo日志时,采用的方式是十分简单粗暴的,也就是直接往里堆,写完一条紧接着写另一条,各条undo日志是亲密无间的,写完一个undo页面后,再从段中申请一个新的页面,然后把这个页面插入到undo页面的链表中,继续往这个新的页面写undo日志,同一个事务向一个undo页面链表中写入的undo日志算是一个组。Undo log header就是记录组的属性的

小结:
对于没有被重用的undo页面链来说,链表中的第一个页面在真正写入undo日志前,会填充Undo Page Header、Undo Log Segment Header、
Undo Log Header这三个部分,之后才真正的写入undo日志,对于其他页来说,在真正写入undo日志前,只会填充 Undo Page Header
链表的基节点放在Undo Log segment Header部分,链表节点信息存放到每个Undo页面的Undo Page Header部分

八、重用undo日志

为了提高并发执行的多个事务写入undo日志的性能,innodb为每个事务单独分配了相应的undo页面链表,但是这也造成了一些问题,比如大部分事务在执行过程中可能只修改了一条或几条记录,针对某个undo页面链表来说产生了非常少的undo日志,这些undo日志可能占用非常少的存储空间,如果每开启一个事务就创建一个undo页面链表(虽然这个链表只有一个页面)来存储这么一点undo日志实在太浪费了,因此就得有重用。一个undo页面链表如果可以被重用,那么它需要符合下面两个条件:
1、该链表中只包含一个undo页面
2、该页面中已经使用小于整个页面空间得3/4
Undo 页面链表可以被分为 insert undo 链表和update undo链表的重用策略也不一样
1、Insert undo 链表直接重用,可以覆盖之前的日志
2、update undo链表中的日志不能被删除,只能在其后面追加

九、回滚段:

事务在执行过程中,最多可以分配四个undo页面链表,同一时刻,不同的事务拥有的undo页面链表是不一样的,系统在同一时刻,其实可以存在许多个Undo页面链表,为了更好地管理这些页面链表,innodb设计了一个为Rollback Segment Header的页面。这个页面存放了各个Undo页面链表的页号,这些页号称为Undo slot,每个Rollback Segment Header都对应一个段,这个段被称为回滚段

从回滚段中申请undo页面链表

初始情况下,由于未向任何事务分配Undo页面链表,所以对于一个Rollback Segment Header来说,它的各个undo slot都被设置为一个特殊的值:FIL_NULL,这表示该undo slot不指向任何页面。
1、如果是FIL_NULL,那么就在表空间中新创建一个段(也就是Undo Log Segment)然后从段中申请一个页面作为undo页面链表的第一个页,最后把该undo slot的值设置为刚刚申请的这个页的地址。也就是意味着这个undo slot被分配给了这个事务
2、如果不是FIL_NULL,说明该undo slot已经指向了一个undo链表,也就是说这个undo slot已经被别的事务占用了,这就需要跳到下一个undo slot,然后判断undo slot是否为FIL_NULL
一个Rollback Segment Header页面中包含了1024个undo slot。如果这1024个undo slot的值都不为FIL_NULL,此时新事务,无法再获得新的Undo页面链表,就会停止执行这个事务并且向用户报错:Too many active concurrent transactions
用户看到这个错误就可以选择重新执行这个事务(可能重新执行事务的时候,有别的事务提交了,该事务就可以被分配undo页面链表了)
当一个事务提交时,它所占用的undo slot有两种命运
1、如果该undo slot指向的undo页面链表符合被重用的条件(就是undo页面链表只占用一个页面,并且已使用空间小于整个页面的3/4),该undo slot就处于被缓存的状态
被缓存的undo slot会被加入到一个链表中,不同类型的undo页面链表对应的undo slot会被加入到不同的链表中
①如果对应的undo页面链表是insert undo 链表,则该链表对应的undo slot会被加入到insert undo cached链中。
②如果对应的Undo页面链表是update undo链表,则undo slot会被加入update undo cached链表中
一个回滚段对应着上述两个catched链表,如果有新的事务要分配undo slot,都会先从对应的catched中查找,如果没有找到被缓存的undo slot,才会到回滚段的Rollback Segment Header页面中寻找
2、如果该undo slot指向的Undo页面链表不符合被重用的条件
①如果对应的undo页面链表是insert undo 链表,就会把TRX_UNDO_STATE设置为释放状态,然后该页面链表对应的段也会被释放掉,然后把该undo slot的值设置为FIL_NULL
②如果是update undo链表,则该页面链表的TRX_UNDO_STATE会被设置TRX_UNDO_TO_PURGE,并将该undo slot的值设置为FIL_NULL,然后将本次事务写入到一组undo日志放到history链表中

多个回滚段
一个事务最多对应4个undo页面链表,而一个回滚段只有1024个undo slot,很显然,undo slot的数量有点少,因此innodb定义了128个回滚段,每个回滚段都对应一个Rollback Segment Header页面,那128个页的地址放哪?系统表空间的第5号页面

回滚段的分类:
1、第0号、第33到127为一类,针对普通表的记录改动设计的
2、第1到32为一类,针对临时表设计的

也就说,如果一个事务在执行过程中既对普通表进行了修改,又对临时表进行了修改,那么就得分配两个回滚段
为什么分不同的回滚段?
Undo页面其实是一个类型为FIL_PAGE_UNDO_LOG的页面,说到底也是一个普通额页,前面说过,在修改页之前,一定要先把对应的redo日志写上,这样系统因崩溃而重启时,才能恢复到崩溃前的状态。向undo页面写入undo日志本身也是一个写页面的过程,设计innodb的还特意设计了许多redo日志的类型,比如,MLOG_UNDO_HDR_CREATE、MLOG_UNDO_INSERT、MLOG_UNDO_INIT。也就是说我们对undo页面做的任何改动都会记录到相应的undo日志。
但是对临时表来说,因为修改临时表产生的undo日志只需在系统运行时有效,如果系统发生崩溃,那么在重启时,也不需要恢复这些undo日志所在的页面。所以在针对临时表写相应的undo页面时,并不需要记录相应的redo日志。针对普通表和临时表划分不同种类的回滚段原因可以总结为:针对修改普通表的回滚段中的undo页面时,需要记录对应的redo日志;而修改针对临时表的回滚段的undo页面时,不需要记录对应的redo日志

十、为事务分配undo页面链表的详细过程

1、事务在执行过程中对普通表的记录进行首次改动之前,首先会到系统表空间的第5号页面中分配一个回滚段(其实就是获取一个Rollback Segment Header页面的地址)一旦某个回滚段被分配了这个事务,那么时候该事务再对普通表的记录进行修改时,就不会重复分配了
使用roud-robin(循环使用)的方式分配回滚段,比如当前的事务分配了0号,那么下一个就是33,简单来说就是这些回滚段被轮询分配给事务
2、在分配到回滚段后,首先看一下这个回滚段中的两个cached链表有没有已经缓存的undo clot。如果事务执行的是INSERT操作,就去回滚段对应的insert undo cached链表中看看有没有缓存的undo clot;如果事务执行的是DELETE操作,就去回滚段对应的update undo cachaed链表中看看有没有缓存的undo slot。如果有缓存的undo slot,就把这个缓存的undo slot分配给事务
3、如果没有缓存的undo slot可以分配,那么就要到Rollback Segment Header找一个可用的undo slot分配给当前事务,就是如果从第0个开始遍历1024和undo slot找到第一个undo slot的值为FIL_NULL的,分配给事务,如果找不到,直接报错
4、找到可用的undo slot后,如果该undo slot是从cached链表中获取的,那么他对应的Undo Log Segment就已经分配了;否则需要重新分配一个Undo Log Segment,然后从这个Undo Log Segment中申请一个页作为undo页面链表的first undo page,并把该页的页号填入到获取的undo slot中
5、然后事务就可以把undo日志写入到上面申请的undo页面的链表中了

对临时表的的记录进行改动,步骤和上面一样,这里不在赘述。不过再次强调一次,如果一个事务在执行过程中,既对普通表的记录进行了改动,又对临时表的记录进行了改动,那么需要为这个事务分配两个回滚段。并发执行的不同事务其实也可以被分配相同的回滚段,只要分配不同的undo slot就可以

十一、Undo log在崩溃恢复时的作用

在服务器因崩溃而恢复的过程中,首先需要按照redo日志将各个页面的数据恢复到崩溃之前的状态,这样就可以保证已经提交的事务的持久性。但是这里仍然存在一个问题,就是那些没有提交的事务写的redo日志可能也已经刷盘,那么这些未提交的事务修改过的页面在mysql服务器重启时可能也恢复了。
为了保证事务的原子性,就有必要在事务重启时,将这些未提交的时候回滚掉,那么怎么找到这些未提交的事务,任务就落在了undo日志的头上
我们可以在系统表空间的第5号页面定位到128个回滚段的位置,在每个回滚段的1024个undo slot中找到那些不为FIL_NULL的undo slot,每一个slot对应一个undo页面链表,然后从Undo页面链表第一个页面的Undo Log Segment Header中找到TRX_UNDO_STATE属性,该属性表示当前页面链表所处的状态,如果该属性的值为TRX_UNDO_ACTIVE,则意味着有一个活跃的事务正在向这个undo 页面链表中写入undo 日志,然后再在Undo Segment Header 中找到TRX_UNDO_LAST_LOG属性,通过该属性可以找到本undo页面链表最后一个Undo Log Header的位置,从该Undo Log Header中找到对应事务的事务id以及一些其他信息,则该事务id对应的事务就是未提交的事务,通过undo日志中记录的信息将该事务对页面所做的更改全部回滚掉,这样就保证了事务的原子性。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/839227.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号