[mysql]mysql的原子性是怎么实现的?(undolog的实现原理)

目录

前言

回答

1. 从redolog的角度(从这一点答出来,应该会让面试官很惊讶吧哈哈)

 2.从undolog的角度

事务id是怎么生成的

roll_pointer隐藏列的含义

delete操作对应的redolog

加入到链表的头节点处与PAGE_GARBAGE

update对应的undolog

不更新主键的情况

更新主键的情况

版本链

总结

内容摘抄


前言

面试被问到了吗,感觉还蛮重要的。

没看看redolog和undolog的实现,还真不敢说这个原子性是怎么实现的。

看完觉得主要分为两方面吧。

主要参考文章:https://juejin.cn/book/6844733769996304392/section/6844733770067607566

[mysql]innodb的redolog

回答

1. 从redolog的角度(从这一点答出来,应该会让面试官很惊讶吧哈哈)

规定在执行这些需要保证原子性的操作时必须以组的形式来记录的redo日志,在进行系统崩溃重启恢复时,针对某个组中的redo日志,要么把全部的日志都恢复掉,要么一条也不恢复。

上一篇笔记的时候,就觉得这个点也算是原子性很重要的一点。

即:以组的形式来进行redo log的恢复,要么都成功,要么都失败!

 2.从undolog的角度

首先只有在事务对表中的记录做改动时才会为这个事务分配一个唯一的事务id;

事务id是怎么生成的

  • 服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个事务id时,就会把该变量的值当作事务id分配给该事务,并且把该变量自增1。

  • 还有一些巴勒巴拉的操作其实也无所谓记住

为了实现事务的原子性InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志:(每对一条记录的主键值做改动时,会记录2条undo日志,因为会有对该记录进行delete mark操作前,会记录一条类型为TRX_UNDO_DEL_MARK_RECundo日志;之后插入新记录时,会记录一条类型为TRX_UNDO_INSERT_RECundo日志)

undo日志的格式

1.一次事务如果有很多undolog,会进行编号,比如undolog 1, 2, 3 就是undo no

2.undo日志是被记录到类型为FIL_PAGE_UNDO_LOG的页面中(专门给undo log 弄一个页面)

3.每个表都会被分配一个唯一的table id

4.INSERT操作对应的undo日志

  • 如果希望回滚插入操作,那么把这条记录删除就好了,也就是说在写对应的undo日志时,主要是把这条记录的主键信息记上。类型为TRX_UNDO_INSERT_RECundo日志

 其实对于undo日志结构,就是有地址,然后列的真实值,然后还有对应的记录表的tableid,然后undo no,还有是什么类型,next 记录的地址,都很好理解。

  • 当我们向某个表中插入一条记录时,实际上需要向聚簇索引和所有的二级索引都插入一条记录。不过记录undo日志时,我们只需要考虑向聚簇索引插入记录时的情况就好了,因为其实聚簇索引记录和二级索引记录是一一对应的,我们在回滚插入操作时,只需要知道这条记录的主键信息,然后根据主键信息做对应的删除操作,做删除操作时就会顺带着把所有二级索引中相应的记录也删除掉。(其实就是只需要向聚蔟索引插数据就好,二级索引回滚会自动删除)
  • undo日志中的某些属性进行压缩处理,挺好的设计,节约空间
  • 别的结构 update 和delete 都差不多吧~感觉大体上都是思想

roll_pointer隐藏列的含义

roll_pointer的真实面纱了,指向记录对应的undo日志的一个指针。

(其实我觉得这个跟页面的结构串起来了,可以给面试官讲讲)

image_1d65h98l3qve1ekb13epv4f37685.png-70.6kB

从图中也可以更直观的看出来,roll_pointer本质就是一个指针,指向记录对应的undo日志。

delete操作对应的redolog

 正常情况:插入到页面中的记录会根据记录头信息中的next_record属性组成一个单向链表

被删除的记录其实也会根据记录头信息中的next_record属性组成一个链表,只不过这个链表中的记录占用的存储空间可以被重新利用,所以也称这个链表为垃圾链表

(注意这个垃圾链表并不是独立出来的吗,而是,就是利用原本的空间,然后更改指针位置)

我觉得可以认为就是可以从比如pagefree的节点,依次往下找到被删除的记录。

删除操作对应:(因为会有事务)

1. 仅仅将记录的delete_mask标识位设置为1,其他的不做修改(其实会修改记录的trx_idroll_pointer这些隐藏列的值)。delete mark。 

挺有意思的,这是一个中间状态:

2.当该删除语句所在的事务提交之后,会有专门的线程后来真正的把记录删除掉。所谓真正的删除就是把该记录从正常记录链表中移除,并且加入到垃圾链表中,然后还要调整一些页面的其他信息,比如页面中的用户记录数量PAGE_N_RECS、上次插入记录的位置PAGE_LAST_INSERT、垃圾链表头节点的指针PAGE_FREE页面中可重用的字节数量PAGE_GARBAGE、还有页目录的一些信息等等。这个阶段叫purge

(将被删除记录加入到垃圾链表时,实际上加入到链表的头节点处,会跟着修改PAGE_FREE属性的值。)

为啥加入到头结点处?看下面

加入到链表的头节点处与PAGE_GARBAGE

Page Header部分有一个PAGE_GARBAGE属性,该属性记录着当前页面中可重用存储空间占用的总字节数。每当有已删除记录被加入到垃圾链表后,都会把这个PAGE_GARBAGE属性的值加上该已删除记录占用的存储空间大小。

PAGE_FREE指向垃圾链表的头节点,之后每当新插入记录时,首先判断PAGE_FREE指向的头节点代表的已删除记录占用的存储空间是否足够容纳这条新插入的记录,如果不可以容纳,就直接向页面中申请新的空间来存储这条记录(是的,你没看错,并不会尝试遍历整个垃圾链表,找到一个可以容纳新记录的节点)。如果可以容纳,那么直接重用这条已删除记录的存储空间,并且把PAGE_FREE指向垃圾链表中的下一条已删除记录。

(其实就是判断头后面的被删除的记录可不可以被重用)

PAGE_GARBAGE这个东西到底是啥呢?

其实就是:如果新插入的那条记录占用的存储空间大小小于垃圾链表的头节点占用的存储空间大小,那就意味头节点对应的记录占用的存储空间里有一部分空间用不到,这部分空间就被称之为碎片空间。那这些碎片空间岂不是永远都用不到了么?其实也不是,这些碎片空间占用的存储空间大小会被统计到PAGE_GARBAGE属性中,这些碎片空间在整个页面快使用完前并不会被重新利用,不过当页面快满时,如果再插入一条记录,此时页面中并不能分配一条完整记录的空间,这时候会首先看一看PAGE_GARBAGE的空间和剩余可利用的空间加起来是不是可以容纳下这条记录,如果可以的话,InnoDB会尝试重新组织页内的记录,重新组织的过程就是先开辟一个临时页面,把页面内的记录依次插入一遍,因为依次插入时并不会产生碎片,之后再把临时页面的内容复制到本页面,这样就可以把那些碎片空间都解放出来(很显然重新组织页面内的记录比较耗费性能)。

(这样也太节省资源了吧,这个过程真的很有趣)

TRX_UNDO_DEL_MARK_REC的结构:

大体看一下就行:

都差不多的玩法。

update对应的undolog

在执行UPDATE语句时,InnoDB对更新主键和不更新主键这两种情况有截然不同的处理方案。

不更新主键的情况

在不更新主键的情况下,又可以细分为被更新的列占用的存储空间不发生变化和发生变化的情况。

  • 就地更新(in-place update)(直接在原记录的基础上修改对应列的值,任何值的存储空间都不变)

  • 先删除掉旧记录,再插入新记录。 (在不更新主键的情况下,如果有任何一个被更新的列更新前和更新后占用的存储空间大小不一致,那么就需要先把这条旧的记录从聚簇索引页面中删除掉,然后再根据更新后列的值创建一条新的记录插入到页面中。)

  • 删除是真正的删除掉,也就是把这条记录从正常记录链表中移除并加入到垃圾链表,并且修改页面中相应的统计信息(比如PAGE_FREEPAGE_GARBAGE等这些信息)。用户线程同步执行真正的删除操作,真正删除之后紧接着就要根据各个列更新后的值创建的新记录插入。

  • (我这边有个问题,那岂不是,频繁的update,最后总要有重新迁移表的过程?,其实就是页分裂,进行插入操作)

  • 这里如果新创建的记录占用的存储空间大小不超过旧记录占用的空间,那么可以直接重用被加入到垃圾链表中的旧记录所占用的存储空间,否则的话需要在页面中新申请一段空间以供新记录使用,如果本页面内已经没有可用的空间的话,那就需要进行页面分裂操作,然后再插入新记录。

更新主键的情况

在聚簇索引中,记录是按照主键值的大小连成了一个单向链表的,如果我们更新了某条记录的主键值,意味着这条记录在聚簇索引中的位置将会发生改变,比如你将记录的主键值从1更新为10000,如果还有非常多的记录的主键值分布在1 ~ 10000之间的话,那么这两条记录在聚簇索引中就有可能离得非常远,甚至中间隔了好多个页面。针对UPDATE语句中更新了记录主键值的这种情况,InnoDB在聚簇索引中分了两步处理:

  • 将旧记录进行delete mark操作(初步标记)

    UPDATE语句所在的事务提交前,对旧记录只做一个delete mark操作,在事务提交后才由专门的线程做purge操作,把它加入到垃圾链表中。这里一定要和上边所说的在不更新记录主键值时,先真正删除旧记录,再插入新记录的方式区分开!

    小贴士: 之所以只对旧记录做delete mark操作,是因为别的事务同时也可能访问这条记录,如果把它真正的删除加入到垃圾链表后,别的事务就访问不到了。这个功能就是所谓的MVCC,我们后边的章节中会详细唠叨什么是个MVCC。

    握草,我的mvcc又精进了一步

  • 根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中(需重新定位插入的位置)。

    由于更新后的记录主键值发生了改变,所以需要重新从聚簇索引中定位这条记录所在的位置,然后把它插进去。

针对UPDATE语句更新记录主键值的这种情况,在对该记录进行delete mark操作前,会记录一条类型为TRX_UNDO_DEL_MARK_RECundo日志;之后插入新记录时,会记录一条类型为TRX_UNDO_INSERT_RECundo日志,也就是说每对一条记录的主键值做改动时,会记录2条undo日志

版本链

执行完delete mark操作后,它对应的undo日志和INSERT操作对应的undo日志就串成了一个链表。这个很有意思啊,这个链表就称之为版本链 。

比如删除会把删除操作和insert操作串起来,组成版本链。

总结

1. 之所以只对旧记录做delete mark操作,是因为别的事务同时也可能访问这条记录,如果把它真正的删除加入到垃圾链表后,别的事务就访问不到了。这个功能就是所谓的MVCC,我们后边的章节中会详细唠叨什么是个MVCC。

这点很重要,丰富mvcc的体系!

内容摘抄

来自:

https://juejin.cn/book/6844733769996304392/section/6844733770067607566

作者也写书了,有机会一定支持一下!

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值