前文我们介绍了 InnoDB 存储引擎在事务隔离级别 READ COMMITTED 和 REPEATABLE READ(默认)下会开启一致性非锁定读,简单回顾下:所谓一致性非锁定读就是每行记录可能存在多个历史版本,多版本之间串联起来形成了一条版本链,这样不同时刻启动的事务可以无锁地访问到不同版本的数据。

undo log 版本链

一致性非锁定读是通过 MVCC(Multi Version Concurrency Control,多版本并发控制) 来实现的。事实上,MVCC 没有一个统一的实现标准,所以各个存储引擎的实现机制不尽相同。

InnoDB 存储引擎中 MVCC 的实现是通过 undo log 来完成的,undo log 是啥?

简单理解,undo log 就是每次操作的反向操作,比如比如当前事务执行了一个插入 id = 100 的记录的操作,那么 undo log 中存储的就是删除 id = 100 的记录的操作。

所以,这里用多版本来形容并不是非常准确,因为 InnoDB 并不会真正地去开辟空间存储多个版本的行记录,只是借助 undo log 记录每次写操作的反向操作。

也就是说,B+ 索引树上对应的记录只会有一个最新版本,只不过 InnoDB 可以根据 undo log 得到数据的历史版本,从而实现多版本控制。

那么,还有个问题,undo log 是如何和某条行记录产生联系的呢?换句话说,我怎么能通过这条行记录找到它拥有的 undo log 呢?

具体来说,InnoDB 存储引擎中每条行记录其实都拥有两个隐藏的字段:trx_idroll_pointer

从名字也能看出来,trx_id 就是最近更新这条行记录的事务 ID,roll_pointer 就是指向之前生成的 undo log。

掏出我们的 user 表,来举个例子,假设 id = 100 的事务 A 插入一条行记录(id = 1, username = "Jack", age = 18),那么,这行记录的两个隐藏字段 trx_id = 100roll_pointer 指向一个空的 undo log,因为在这之前并没有事务操作 id = 1 的这行记录。如图所示:

然后,id = 200 的事务 B 修改了这条行记录,把 age 从 18 修改成了 20,于是,这条行记录的 trx_id 就变成了 200,rooll_pointer 就指向事务 A 生成的 undo log :

接着,id = 300 的事务 C 再次修改了这条行记录,把 age 从 20 修改成了 30,如下图:

可以看到,每次修改行记录都会更新 trx_id 和 roll_pointer 这两个隐藏字段,之前的多个数据快照对应的 undo log 会通过 roll_pointer 指针串联起来,从而形成一个版本链

需要注意的是,select 查询操作不会生成 undo log!在 InnoDB 存储引擎中,undo log 只分为两种:

  • insert undo log:在 insert 操作中产生的 undo log
  • update undo log:对 delete 和 update 操作产生的 undo log

事实上,由于事务隔离性的要求,insert 操作的记录,只对事务本身可见,对其他事务不可见,对吧,所以也就不存在并发情况下的问题。所以,也就是说,MVCC 这个机制,其实就是靠 update undo log 实现的,和 insert undo log 基本上没啥关系,我们上面说的 undo log 版本链上的其实就是 update undo log。

ReadView 机制

说到 MVCC,说到 undo log 版本链,如果你自己不往下说的话,八九不离十面试官都会问你下 ReadView 这个机制。

咱也不卖官子,直接说吧,ReadView 机制就是用来判断当前事务能够看见哪些版本的,一个 ReadView 主要包含如下几个部分:

  • m_ids:生成 ReadView 时有哪些事务在执行但是还没提交的(称为 ”活跃事务“),这些活跃事务的 id 就存在这个字段里
  • min_trx_id:m_ids 里最小的值
  • max_trx_id:生成 ReadView 时 InnoDB 将分配给下一个事务的 ID 的值(事务 ID 是递增分配的,越后面申请的事务 ID 越大)
  • creator_trx_id:当前创建 ReadView 事务的 ID

接下来,再掏出 user 表,通过一个例子来理解下 ReaView 机制是如何做到判断当前事务能够看见哪些版本的:

假设表中已经被之前的事务 A(id = 100)插入了一条行记录(id = 1, username = "Jack", age = 18),如图所示:

接下来,有两个事务 B(id = 200) 和 C(id = 300)过来并发执行,事务 B 想要更新(update)这行 id = 1 的记录,而事务 C(select)想要查询这行数据,这两个事务都执行了相应的操作但是还没有进行提交:

如果现在事务 B 开启了一个 ReadView,在这个 ReadView 里面:

  • m_ids 就包含了当前的活跃事务的 id,即事务 B 和事务 C 这两个 id,200 和 300
  • min_trx_id 就是 200
  • max_trx_id 是下一个能够分配的事务的 id,那就是 301
  • creator_trx_id 是当前创建 ReadView 事务 B 的 id 200

现在事务 B 进行第一次查询(上面说过 select 操作不会生成 undo log 的哈),会把这行记录的隐藏字段 trx_id 和 ReadView 的 min_trx_id 进行下判断,此时,发现 trx_id 是 100,小于 ReadView 里的 min_trx_id(200),这说明在事务 B 开始之前,修改这行记录的事务 A 已经提交了,所以开始于事务 A 提交之后的事务 B、是可以查到事务 A 对这行记录的更新的

row.trx_id < ReadView.min_trx_id

接着事务 C 过来修改这行记录,把 age = 18 改成了 age = 20,所以这行记录的 trx_id 就变成了 300,同时 roll_pointer 指向了事务 C 修改之前生成的 undo log:

那这个时候事务 B 再次进行查询操作,会发现这行记录的 trx_id(300)大于 ReadView 的 min_trx_id(200),并且小于 max_trx_id(301)

row.trx_id > ReadView.min_trx_id && row.trx_id < max_trx_id

这说明一个问题,就是更新这行记录的事务很有可能也存在于 ReadView 的 m_ids(活跃事务)中。所以事务 B 会去判断下 ReadView 的 m_ids 里面是否存在 trx_id = 300 的事务,显然是存在的,这就表示这个 id = 300 的事务是跟自己(事务 B)在同一时间段并发执行的事务,也就说明这行 age = 20 的记录事务 B 是不能查询到的。

既然无法查询,那该咋整?事务 B 这次的查询操作能够查到啥呢?

没错,undo log 版本链!

这时事务 B 就会顺着这行记录的 roll_pointer 指针往下找,就会找到最近的一条 trx_id = 100 的 undo log,而自己的 id 是 200,即说明这个 trx_id = 100 的 undo log 版本必然是在事务 B 开启之前就已经提交的了。所以事务 B 的这次查询操作读到的就是这个版本的数据即 age = 18。

通过上述的例子,我们得出的结论是,通过 undo log 版本链和 ReadView 机制,可以保证一个事务不会读到并发执行的另一个事务的更新

那自己修改的值,自己能不能读到呢?

这当然是废话,肯定可以读到呀。不过上面的例子我们只涉及到了 ReadView 中的前三个字段,而 creator_trx_id 就与自己读自己的修改有关,所以这里还是图解出来让大家更进一步理解下 ReadView 机制:

假设事务 C 的修改已经提交了,然后事务 B 更新了这行记录,把 age = 20 改成了 age = 66,如下图所示:

然后,事务 B 再来查询这条记录,发现 trx_id = 200 与 ReadView 里的 creator_trx_id = 200 一样,这就说明这是我自己刚刚修改的啊,当然可以被查询到。

row.trx_id = ReadView.creator_trx_id

那如果在事务 B 的执行期间,突然开了一个 id = 400 的事务 D,然后更新了这行记录的 age = 88 并且还提交了,然后事务 B 再去读这行记录,能读到吗?

答案是不能的。

因为这个时候事务 B 再去查询这行记录,就会发现 trx_id = 500 大于 ReadView 中的 max_trx_id = 301,这说明事务 B 执行期间,有另外一个事务更新了数据,所以不能查询到另外一个事务的更新。

row.trx_id > ReadView.max_trx_id

那通过上述的例子,我们得出的结论是,通过 undo log 版本链和 ReadView 机制,可以保证一个事务只可以读到该事务自己修改的数据或该事务开始之前的数据

小结

总结下,通过 undo log 版本链和 ReadView 机制:

  • 可以保证一个事务不会读到并发执行的另一个事务的更新
  • 可以保证一个事务只可以读到该事务自己修改的数据或该事务开始之前的数据

另外,前文说过,一致性非锁定读(或者直接说 MVCC 吧,毕竟一致性非锁定读也是靠 MVCC 实现的)只在事务隔离级别 READ COMMITTED 和 REPEATABLE READ(默认)下才会开启,那对于这两个隔离级别,其实最根本的不同之处,就在于它们生成 ReadView 的时机不同,这个我们留在下文解释~

关注公众号 | 飞天小牛肉,即时获取更新

  • 博主东南大学硕士在读,携程 Java 后台开发暑期实习生,利用课余时间运营一个公众号『 飞天小牛肉 』,2020/12/29 日开通,专注分享计算机基础(数据结构 + 算法 + 计算机网络 + 数据库 + 操作系统 + Linux)、Java 技术栈等相关原创技术好文。关注公众号第一时间获取文章更新,后台回复 300 即可免费获取极客大学出品的 Java 面试 300 题

  • 并推荐个人维护的开源教程类项目: CS-Wiki(Gitee 推荐项目,现已累计 1.8k+ star), 致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习 ~

  • 如果各位小伙伴春招秋招没有拿得出手的项目的话,可以参考我写的一个项目「开源社区系统 Echo」Gitee 官方推荐项目,目前已累计 900+ star,基于 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 并提供详细的开发文档和配套教程。公众号后台回复 Echo 可以获取配套教程,目前尚在更新中。

三分钟图解 MVCC,看一遍就懂的更多相关文章

  1. 看一遍就懂:MVCC原理详解

    MVCC实现原理也是一道非常高频的面试题,自己在整理这篇文章的时候,感觉到网上的资料在讲这块知识点上写的五花八门,好像大家的理解并没有一致. 这里将自己所理解的做一个总结,个人会觉得这是一篇含金量挺高 ...

  2. 十分钟带你看一遍ES6新特性

    let , const关键字 var 看习惯了java, 看js真的是忍不住想笑,比如说这个var,它太自由了,自由到{}根本限制不住它的生命周期 js的var关键字,无论在何处声明,都会被视为声明在 ...

  3. 看一遍就懂,详解java多线程——volatile

    多线程一直以来都是面试必考点,而volatile.synchronized也是必问点,这里我试图用容易理解的方式来解释一下volatile. 来看一下它的最大特点和作用: 一 使变量在多个线程间可见 ...

  4. 演进之美,越来越美:三分钟看尽 iOS 1 ~ iOS 8 的进化史

    演进之美,越来越美:三分钟看尽 iOS 1 ~ iOS 8 的进化史 原文出处: 少数派 9 月 18 日苹果就将推出 iOS 8 正式版了,从 2007 年发布第一代 iPhone 时搭载在 iPh ...

  5. unity3d 三分钟实现简单的赛车漂移

    提到赛车游戏,大家最关心的应该就是漂移吧?! 从学unity开始,我就一直在断断续续的研究赛车 因为自己技术太烂.悟性太差等原因,我走了不少弯路 也许你会说,网上那么多资料,你不会查啊 是啊!网上一搜 ...

  6. JUnit三分钟教程 ---- 实际应用

    JUnit三分钟教程 ---- 实际应用 摘自http://lavasoft.blog.51cto.com/62575/65775   接上文"JUnit三分钟教程 ---- 快速起步&qu ...

  7. JUnit三分钟教程 ---- 快速起步

    JUnit三分钟教程 ---- 快速起步 摘自http://lavasoft.blog.51cto.com/62575/65625/ JUnit是个好东西,做大点的项目离不开这东西,实际中用的时候也因 ...

  8. 三分钟浅谈TT猫的前端优化

    首先看一张访问TT猫首页的截图: 测试环境为谷歌浏览器,暂且不讨论其它浏览器,截图下方我们可以观察到以下参数: DOMContentLoaded:1.42s | Load:2.31s 以上参数是在CT ...

  9. windows+mysql集群搭建-三分钟搞定集群

    注:本文来源:  陈晓婵   <  windows+mysql集群搭建-三分钟搞定集群   > 一:mysql集群搭建教程-基础篇 计算机一级考试系统要用集群,目标是把集群搭建起来,保证一 ...

随机推荐

  1. ajax前后台通信验错

    目录 默认contenType下 总结以下例证: 1.当为简单JSON时 ajax controller vo 2.当为复杂JSON的时候 ajax controller vo 结果 contentT ...

  2. 关于SequenceInputStream

    两个流合并时: package stream.sequence; import java.io.BufferedReader; import java.io.BufferedWriter; impor ...

  3. 管理 Python 多版本,pyenv 用起来

    介绍 学习使用pyenv在本地安装多个 Python 版本,这样既不影响工作,也不影响生活~ pyenv 可让你轻松地在多个 Python 版本之间切换.它简单.不引人注目,并且遵循 UNIX 的单一 ...

  4. Element form表单方法resetFields无效

    之前遇到resetFields无效时都是自己手动用this.ruleForm = Object.assign({}, this.ruleForm, this.$options.data().ruleF ...

  5. Promise.resolve( data)与Promise.reject( data )

    Promise.resolve( data)与Promise.reject( data ) 常用来生成已经决议失败或成功的promise实例: 1.Promise.reject(data)不管传递的是 ...

  6. Defence

      emm...这道题我调了一下午你敢信??   好吧还是我太天真了.   开始的时候以为自己线段树动态开点与合并写错了,就调;   结果发现没问题,那就是信息维护错了.   一开始以为自己最左右的1 ...

  7. Flask(6)- debug 模式

    使用 Flask 开发过程中存在两个常见的问题 当 Flask 程序出错时,没有提示错误的详细信息 修改 Flask 源代码后需要重启 Flask 程序 这两个问题非常的影响开发效率,因此 Flask ...

  8. docker镜像与容器的导出导入

    导入导出涉及的命令有save.load.export.import # 1) docker save 导出镜像到文件 docker save -o nginx.tar nginx:latest # 2 ...

  9. JS008. 跳转缓存滚动条高度并返回时过渡动画(window.pageYOffset & window.scrollTo & SessionStorage)

    业务场景 从列表跳转详情页,通过操作返回列表页时,滚动条仍然处于跳转前的高度,并加上 ease-out 的过渡动画. 由于sessionStorage是随页面即关即消的,所以比起VUEX.localS ...

  10. js实现钟表

    在网页上显示一个钟表 html: <body onload="startTime()"> <div id="txt"></div& ...