您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
innodb中MVCC实现原理浅析
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
innodb中MVCC实现原理浅析
自猿其说Tech
2021-09-15
IP归属:未知
36080浏览
计算机编程
Sql
### 1 数据库事务 #### 1.1 从事物说起 事物是一组逻辑上的操作,这组操作将执行与否将会保持一致,而在数据库层面,它是一个或多个数据库操作。下面将从事物的状态、条件以及隔离等问题展开(熟悉的读者可以直接略过~) ##### 1)事物的状态 事物对应着一组操作,大致划分成以下五种状态: - Active(活动):此时事务对应的数据库操作正在执行过程中 - Partially Committed(部分提交):当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的修改并未刷新到磁盘时 - Failed(失败):当事务处在Active或者Partially Committed状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行时,此时该事务处在失败状态 - Aborted(中止):如果事务执行过程中变为Failed状态,需要撤销失败事务对当前数据库造成的影响,这个过程称为回滚。当回滚操作执行完毕,数据库恢复到了执行事务之前的状态,此时事务处在了中止的状态。 - committed(已提交):当处在部分提交的状态的事务将修改过的数据同步到磁盘后,此时该事务处在了提交的状态。 ##### 2)事物的特性 - 原子性:事务是一个不可分割的工作单元,事务中包括的诸操作要么都做,要么都不做。 - 一致性:事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。 - 隔离性:事务的执行不能被其他事务干扰,事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 - 持久性:也称永久性,指事务一旦提交,它对数据库中数据的改变就应该是永久性的。 ##### 3)事物的隔离问题 多个线程并发的执行多个事物进行数据库操作时,会产生一些新的问题: - 读-读:不会存在任何线程安全问题。 - 写-写:有线程安全问题,可能造更新丢失 - 第一类更新丢失:回滚丢失; - 第二类更新丢失:覆盖丢失。 - 写-读:有线程安全问题,可能造成事务隔离性问题 - 脏读:事物A未提交,但事物B就可以看到事物A的数据; - 不可重复读:一个事物范围内,多次查询相同数据结果不一致; - 幻读:当前读的情况下错误读取了其他事物新增的数据。 ##### 事4)事物的隔离级别 应对事务并发执行的问题,有这么几种方法来解决: - 读未提交(RU):事物还未提交时的数据也可以被其他事物读取 - - 问题:可能会导致脏读、不可重复读和幻读; - 读已提交(RC):事物提交后的结果才会被其他事物读取 - - 问题:可能会导致不可重复读和幻读; - 可重复读(RR):多次读取时取第一次查询的快照,保证查询期间数据的一致性,这是Mysql在innodb引擎下的默认隔离级别; - - 问题:可能会导致幻读 - 序列化(SER):可避免所有线程安全问题。 - - 问题:无并发安全问题,但开销最大 #### 1.2 什么是MVCC MVCC的英文全称为Multi-Version Concurrency Control,翻译成中文就是多版本并发控制,实际上是一种用来解决事物并发冲突的无锁机制。所谓多版本并发控制,指的其实是在多组读写事物并发操作的情况下,为事物分配单向时间戳,为每个修改保存相应的版本信息,并使其和事物时间戳关联,新的读取操作则只获得事物开始前的快照。 因此在MVCC模式下并发读写数据库,可以做到读写操作互不阻塞,提供数据库并发读写性能,并且还可以解决脏读、幻读和不重复读等事物隔离问题。 ### 2 MVCC实现原理 第1章充满了各种概念,仅供各位回顾,下面进入本文的正题:MVCC是怎么实现的。 #### 2.1 Undo日志记录 根据上文的描述,我们知道事物需要保持原子性,也即是对于事物的操作要么全部完成,要么什么也不做。但很多时候很难如愿,会出现事物执行一半的情况,发生这种事情的原因很多,诸如执行过程中服务器的错误、操作系统的错误或者是研发人员主动发起的中止操作,但这个时候已经修改了很多东西,为了保证事物的原子性,我们必须要将这些改变恢复原状,这样的过程称之为回滚。正是有了这样的需求,数据库引擎会对每条记录的修改(增、删、改)留下一条记录,这个记录就是undo日志。 ##### 2.1.1.事物id 事务执行过程中对表执行修改操作,InnoDB引擎会给它分配一个独一无二的事务id,分配方式如下: 1. 只读事物,在它第一次对临时表执行增、删、改操作时才会分配事务id; 1. 读写事务,只在它第一次对某个表(包括临时表)执行增、删、改操作时才会配一个事务id。 事物id是事物的唯一标记,如果读者对innodb记录行格式稍微熟悉的话,就会留意到:聚簇索引的记录除了保存完成的数据之外,还会自动添加两个隐藏的字段,分别为trx_id、roll_pointer。如果表中没有定义主键和唯一键,还会自动条件一个名为row_id的隐藏列,因此在用户数据列之外,一条记录行大概的结构如下: ![](//img1.jcloudcs.com/developer.jdcloud.com/2a25bf7c-a00c-44b6-8a9b-e82c67116da820210915121309.png) ##### 2.1.2 undo日志 为了实现事物原子性的目标,数据库引擎在修改记录的过程中,都会先把对应的undo日志记录下来。根据事物执行过程中的操作数据,这些回滚日志会被编号,一个完成事物执行过程,会产生多条undo日志。通过上文中隐藏列的roll_pointer字段,可以形成一条事物执行过程的undo日志链。对于不同的修改过程,undo日志的生成逻辑也不尽相同。 #### 2.2 版本链 前文讨论过innodb引擎下,聚簇索引记录中包含了两个隐藏列,他们分别是记录事物id的trx_id列和保存旧版本undo日志指针的roll_pointer列,有了这两个列我们故事才可以慢慢继续: 以插入为例,若表中只有一条用户记录,插入时的结构就如下图所示: <center>![](//img1.jcloudcs.com/developer.jdcloud.com/c745504c-551a-49f8-966a-5f489df6399820210915122527.png) 图1:插入时形成undo日志的结构</center> 每当对数据进行修改的时候,都会记录相应的undo日志。每条日志都会有roll_pointer字段,通过这个字段提供的undo日志指针,我们可以把这些日志都串联起来形成一个链表,他们的结构就如同下图: <center>![](//img1.jcloudcs.com/developer.jdcloud.com/6cb1c0c9-9efe-423a-82c5-32742316b1bd20210915122543.png) 图2:多个undo日志形成的版本链</center> 实际上,对用户数据修改后,数据库引擎会将旧值放入undo日志,也即是一个旧的版本。当修改操作的增加,所有记录会连成一个链表,我们将其称之“版本链”,它的头结点便是最新的用户数据,其他节点为undo日志。 其实,undo日志在其他更新情况下,生成的规则也不太一样。考虑到篇幅原因,此处不再展开,感兴趣的同学可以另行学习哈。 #### 2.3 ReadView的结构 前文提到了事物的隔离级别,它们对于读取事物是否提交的记录有不同的要求: - 读未提交(RU):可以读到事物未提交的修改记录,直接读取最新版本; 读已提交(RC)/可重复读(RR):必须保证读到事物已经提交过的修改记录 ##### 2.3.1 ReadView的结构 在事物并发执行的情况下,如何判断版本链中哪些事物是可见的,便成为了此时最关键的问题。Mysql数据库的innoDB引擎在此处引入了一个新的概念——ReadView,它主要包含了以下几个参数: - m_ids:生成ReadView时当前系统中活跃的读写事务的事务id列表; - min_trx_id:m_ids中的最小值; - max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的事物id值,它并不是m_ids事物列表中的最大值,实际上事物id是递增分配的; - creator_trx_id:表示生成该ReadView的事务的事务id。(简单说就是谁用谁生成)。实际上只有当对数据记录修改的时候,才会分配事物id,如果一个纯只读事物,那么它的事物id则默认为0。 ##### 2.3.2 ReadVeiw的可见性规则 - 被访问事物的事物id 等于 ReadView中creator_trx_id相同,版本可被当前事物访问 - - 说明:事务正在访问自己修改过的记录 - 被访问事物的事物id 小于 ReadView中min_trx_id,版本可被当前事物访问 - - 说明:生成该版本的事物在当前事物生成ReadView前已提交 - 被访问事物的事物id 大于 ReadView中max_trx_id,版本不可被当前事物访问 - - 说明:生成该版本的事务在当前事务生成ReadView后才开启 - 被访问事物的事物id 在ReadView的min_trx_id和max_trx_id之间 - - 判断一:trx_id在m_ids列表中,不可被访问 - - - 说明:创建ReadView时生成该版本的事务还是活跃的 - - 判断二:trx_id不在m_ids列表中,可被访问 - - - 说明:创建ReadView时生成该版本的事务已经被提交 如果版本链中当前版本对当前事物不可见,则依顺序找到下一版本继续判断可见性。如果直到最后一个版本也不可见,那么该记录就对事物完全不可见。 #### 2.4 ReadView生成机制 ReadView是MVCC机制下的产物,因此只有在RC(读已提交)和RR(可重复读)的隔离级别下,才会有被生成,并且他们的生成时机也不相同。新建一张表,并由事物id为1的事物查询一条记录 ```sql mysql> SELECT * FROM goods; +--------+---------+----------+ | id | goodsNo | goodsName| +--------+---------+----------+ | 1 | 1234 | 电脑 | +--------+---------+----------+ ``` ##### 2.4.1 读已提交(RC) 读已提交的隔离级别之下,事物在每次读取数据的时候都会生成ReadView。此时有两个事物正在执行,他们的事物id分别为10和20: ```sql # Transaction 10 BEGIN; UPDATE goods SET goodsName = '内存条' WHERE goodsNo = 1234; UPDATE goods SET goodsName = '显示器' WHERE goodsNo = 1234; # Transaction 20 BEGIN; ... ``` 此时的生成的版本链如下: <center>![](//img1.jcloudcs.com/developer.jdcloud.com/174f5855-e218-4740-b5dd-9c605bd2457e20210915122937.png) 图3:读已提交隔离级别下生成的版本链1</center> 由于两个事物均为提交,此时在读已提交的隔离级别下执行以下查询语句: ```sql # 无事物提交 SELECT * FROM goods WHERE goodsNo = 1234; # 得到的goodsName为'电脑' ``` 在读已提交的隔离级别下,每次读取数据前都会生成ReadView,此时生成的ReadView属性分别为: - m_ids列表:[10,20],代表两个正在执行中的事物; - min_trx_id:10,m_ids中的最小值; - max_trx_id:21,递增分配; - creator_trx_id:0,表明生成该readView的事物id。而此时由于没有修改数据的操作,也就不会分别事物id,而为默认的事物id。 根据ReadView,我们来分析下这个查询过程: - 最新版本事物id为10,它处于m_ids列表内,不符合可见性要求; - 其次版本事物id为10,同样不符合要求 - 最后版本事物id为1,它小于min_trx_id。符合可见性的要求,查询结果就取这条记录,得到的goodsName为“电脑”。 然后,再把上文中事物id为10的事物提交,再利用事物id为20的事物更新下同一条记录 ```sql # Transaction 20 BEGIN; UPDATE goods SET goodsName = '键盘' WHERE goodsNo = 1234; UPDATE goods SET goodsName = '鼠标' WHERE goodsNo = 1234; ``` 这个时候问题就会变得稍微复杂,它会生成一个新的版本链 <center>![](//img1.jcloudcs.com/developer.jdcloud.com/95c869ae-001c-4c28-975b-001a9e855adf20210915133737.png) 图4:读已提交隔离级别下生成的版本链2</center> 根据readview的处理原则,可以分析得到: 1. 当事物10和20均为提交,此时得到的值为初始值goodsName值为电脑; 1. 当事物20提交,事物10未提交,此时得到的goodsName值为显示器; 1. 但事物10和20均提交了,此时得到的goodsName值为显示器。 ##### 2.4.2 可重复读(RR) 在可重复读隔离级别的事物中,生成ReadView的机制也有了一些不一样,它只会在首次执行查询的时候生成,之后也会一直沿用此ReadView直至事物结束。和上文相同的例子,我们可以得到一个版本链如下: ```sql # Transaction 10 BEGIN; UPDATE goods SET goodsName = '内存条' WHERE goodsNo = 1234; UPDATE goods SET goodsName = '显示器' WHERE goodsNo = 1234; # Transaction 20 BEGIN; # 更新其他记录 ... ``` <center>![](//img1.jcloudcs.com/developer.jdcloud.com/48a29126-80b3-43fb-9493-803db815067020210915133911.png) 图5:可重复读隔离级别下生成的版本链1</center> 现在有一个使用可重复读(RR)隔离级别的事物开始执行查询,这个时候会生成一条readView: ![](//img1.jcloudcs.com/developer.jdcloud.com/d7d46a29-b50b-46f2-ab51-b065d73ebcd720210915133942.png) 此时,查询的执行过程为: - 从版本链寻找可见记录,最新版本goodsName为显示器,此时事物id为10不符合要求; - 下一版本goodsName为内存条,事物id也不符合要求; - 最后一个版本事物id为1,这个值低于 min_trx_id,是符合版本要求的,因此此时查询的值即为该条记录。 这个时候我们再将事物10提交,事物20做一些更新操作: ```sql # Transaction 20 BEGIN; UPDATE goods SET goodsName = '键盘' WHERE goodsNo = 1234; UPDATE goods SET goodsName = '鼠标' WHERE goodsNo = 1234; ``` 这个时候会形成一条新的版本链: <center>![](//img1.jcloudcs.com/developer.jdcloud.com/2c6d70ba-3ccd-4a5c-b09b-a34087debfcd20210915134031.png) 图6:可重复读隔离级别下生成的版本链2</center> 和RC不同的时候,此时的readview仍然为第一次查询时生成(见图5)。根据2.3节中的ReadVeiw的可见性规则,此时执行查询的情况分别如下: 1. 当事物10和20均为提交,此时得到初始值goodsName值为“电脑”; 1. 当事物10提交,事物20未提交,此时得到的值仍为初始值goodsName值为“电脑”; 1. 只有当事物10和20均提交的时候,才会得到goodsName值为“鼠标”的最新值; ### 3 总结和思考 简单小结下本文内容 1. MVCC日志依靠undo日志实现,而undo日志是数据库原子性的实现基础; 1. 通过undo日志和事物ID的配合,可以生成版本链; 1. ReadView是innodb引擎特有的数据结构,读已提交(RC)和可重复读(RR)时生成时机不同,RC为每次读取事物前生成,RR为第一次读取时生成; 1. 通过上文的分析,可以看出来MVCC是一种在读已提交(RC)和可重复读(RR)两种隔离级别之下,为了解决多个事物之间读写并发问题的一种机制。有别于传统的加锁机制,MVCC可以以更高的效率来解决复杂场景下的并发问题,进而提升系统的性能。 ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:网规技术部 张孝祥
原创文章,需联系作者,授权转载
上一篇:ES缓存配置及优化方向实践
下一篇:Elasticsearch与Clickhouse数据存储对比
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号