您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
MySQL事务及MVCC实现原理详解
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
MySQL事务及MVCC实现原理详解
自猿其说Tech
2021-02-09
IP归属:未知
1077400浏览
计算机编程
>大家好,**我是java小杰要加油**, >今天来分享一个京东面试真题,也是这是我前阵子听我旁边高T(高,实在是高)面试候选人的时候问的一个问题,他问,你能说说 **mysql的事务吗? MVCC有了解吗?** ## 事务定义及四大特性 * **事务是什么?** 就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元。 * 事务的四大特性(简称**ACID**): * **原子性(Atomicity)**:一个事务是一个**不可分割**的工作单位,事务中包括的操作要么都做,要么都不做。 * **一致性(Consistency)**:事务必须是使数据库**从一个一致性状态变到另一个一致性状态**。一致性与原子性是密切相关的。 * **隔离性(Isolation)**:一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间**不能互相干扰**. * **持久性(Durability)**:指一个事务一旦提交,它对数据库中数据的**改变就应该是永久性的**,接下来的其他操作或故障不应该对其有任何影响。 ## 事务中常见问题 * **脏读(dirty read)**:就是一个A事务即便没有提交,它对数据的修改也可以被其他事务B事务看到,B事务读到了A事务还未提交的数据,这个数据有可能是错的,有可能A不想提交这个数据,这只是A事务修改数据过程中的一个中间数据,但是被B事务读到了,这种行为被称作**脏读**,这个数据被称为**脏数据** * **不可重复读(non-repeatable read)**:在A事务内,多次读取同一个数据,但是读取的过程中,B事务对这个数据进行了修改,导致此数据变化了,那么A事务再次读取的时候,数据就和第一次读取的时候不一样了,这就叫做**不可重复读** * **幻读(phantom read)**:A事务多次查询数据库,结果发现查询的数据条数不一样,A事务多次查询的间隔中,B事务又写入了一些符合查询条件的多条数据(这里的写入可以是update,insert,delete),A事务再查的话,就像发生了幻觉一样,怎么突然改变了这么多,这种现象这就叫做**幻读** ## 隔离级别——产生问题的原因 多个事务互相影响,并没有隔离好,就是我们刚才提到的事务的四大特性中的 **隔离性(Isolation)** 出现了问题 事务的隔离级别并没有设置好,下面我们来看下事务究竟有哪几种隔离级别 * 隔离级别 * **读未提交(read uncommitted RU):** 一个事务还没提交时,它做的变更就能被别的事务看到 * **读提交(read committed RC):** 一个事务提交之后,它做的变更才会被其他事务看到。 * **可重复读(repeatable read RR):** 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。 * **串行化(serializable ):** 顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。 我们来看个例子,更加直观的了解这**四种隔离级别**和上述问题**脏读,不可重复读,幻读**的关系 ![](https://oncexhj.oss-cn-beijing.aliyuncs.com/%E6%B5%81%E7%A8%8B%E5%9B%BE_20210103202350.PNG) 下面我们讨论下当事务处于**不同隔离级别**情况时,V1,V2,V3分别是什么不同的值吧 * **读未提交 (RU):** A事务可以读取到B事务修改的值,即便B事务没有提交。所以V1就是200 * V1 : 200 * V2 : 200 * V3 : 200 * **读提交(RC):** 当B事务没有提交的时候,A事务不可以看到B事务修改的值,只有提交以后才可以看到 * V1 : 100 * V2 : 200 * V3 : 200 * **可重复读(RR):** A事务多次读取数据,数据总和第一次读取的一样, * V1 : 100 * V2 : 100 * V3 : 200 * **串行化(S):** 事务A在执行的时候,事务B会被锁住,等事务A执行结束后,事务B才可以继续执行 * V1 : 100 * V2 : 100 * V3 : 200 ## MVCC原理 MVCC(Multi-Version Concurrency Control)多版本并发控制,是数据库控制并发访问的一种手段。 >* 特别要注意**MVCC**只在 **读已提交(RC)** 和 **可重复度(RR)** 这两种事务隔离级别下才有效 >* 是 **数据库引擎(InnoDB)** 层面实现的,用来处理读写冲突的手段(不用加锁),提高访问性能 MVCC是怎么实现的呢?它靠的就是**版本链**和**一致性视图** ### 1. 版本链 * 版本链是一条链表,链接的是每条数据曾经的修改记录 **那么这个版本链又是如何形成的呢,每条数据又是靠什么链接起来的呢?** 其实是这样的,对于InnoDB存储引擎的表来说,它的聚簇索引记录包含两个隐藏字段 * **trx_id:** 存储修改此数据的事务id,只有这个事务操作了某些表的数据后当更改操作发生的时候(update,delete,insert),才会分配唯一的事务id,并且此事务id是递增的 * **roll_pointer:** 指针,指向上一次修改的记录 * row_id(非必须): 当有主键或者有不允许为null的unique键时,不包含此字段 假如说当前数据库有一条这样的数据,假设是事务ID为100的事务插入的这条数据,那么此条数据的结构如下 ![](https://oncexhj.oss-cn-beijing.aliyuncs.com/image_20210109142824.png) 后来,事务200,事务300,分别来修改此数据 | 时间T |trx_id 200 | trx_id 300 | | --- | --- | --- | | T1 | 开始事务 | 开始事务 | | T2 | 更改名字为A | | | T3 | 更改名字为B | | | T4 | 提交事务 | 更改名字为C | | | T6 | | 提交事务 | 所以此时的版本链如下 ![](https://oncexhj.oss-cn-beijing.aliyuncs.com/image_20210109142959.png) 我们每更改一次数据,就会插入一条undo日志,并且记录的**roll_pointer**指针会指向上一条记录,如图所示 1. 第一条数据是小杰,事务ID为100 2. 事务ID为200的事务将名称从小杰改为了A 3. 事务ID为200的事务将名称从A又改为了B 3. 事务ID为300的事务将名称从B又改为了C 所以串成的链表就是 C -> B -> A -> 小杰 (从最新的数据到最老的数据) ### 2. 一致性视图(ReadView) 需要判断版本链中的哪个版本是是当前事务可见的,因此有了一致性视图的概念。其中有四个属性比较重要 * **m_ids:** 在生成ReadView时,当前活跃的读写事务的事务id列表 * **min_trx_id:** m_ids的最小值 * **max_trx_id:** m_ids的最大值+1 * **creator_trx_id:** 生成该事务的事务id,单纯开启事务是没有事务id的,默认为0,creator_trx_id是0。 版本链中的当前版本是否可以被当前事务可见的要根据这四个属性按照以下几种情况来判断 * 当 trx_id = creator_trx_id 时:当前事务可以看见自己所修改的数据, **可见**, * 当 trx_id < min_trx_id 时 : 生成此数据的事务已经在生成readView前提交了, **可见** * 当 trx_id >= max_trx_id 时 :表明生成该数据的事务是在生成ReadView后才开启的, **不可见** * 当 min_trx_id <= trx_id < max_trx_id 时 * trx_id 在 m_ids 列表里面 :生成ReadView时,活跃事务还未提交,**不可见** * trx_id 不在 m_ids 列表里面 :事务在生成readView前已经提交了,**可见** ![](https://oncexhj.oss-cn-beijing.aliyuncs.com/image_20210109160159.png) 如果某个版本数据对当前事务不可见,那么则要顺着版本链继续向前寻找下个版本,继续这样判断,以此类推。 > 注:**RR和RC生成一致性视图的时机不一样** (这也是两种隔离级别实现的主要区别) > * 读提交(read committed RC) 是在**每一次**select的时候生成ReadView的 > * 可重复读(repeatable read RR)是在**第一次**select的时候生成ReadView的 下面咱们一起来举个例子实战一下。 ## RR与RC和MVCC的例子实战 假如说,我们有多个事务如下执行,我们通过这个例子来分析当数据库隔离级别为RC和RR的情况下,当时读数据的**一致性视图**和**版本链**,也就是**MVCC**,分别是怎么样的。 > * 假设数据库中有一条初始数据 姓名是java小杰要加油,id是1 (id,姓名,trx_id,roll_point),插入此数据的事务id是1 >* 尤其要指出的是,只有这个事务操作了某些表的数据后当更改操作发生的时候(update,delete,insert),才会分配唯一的事务id,并且此事务id是递增的,单纯开启事务是没有事务id的,默认为0,creator_trx_id是0。 > * 以下例子中的A,B,C的意思是将姓名更改为A,B,C 读也是读取当前时刻的姓名,默认全都开启事务,并且此事务都经历过某些操作产生了事务id | 时间 | 事务100 | 事务200 | 事务300 | 事务400 | | --- | --- | --- | --- | --- | | T1 | A | | | | | T2 | B | | | | | T3 | | C | | | | T4 | | | 读 | | | T5 | 提交 | | | | | T6 | | | D | | | T7 | | | | 读 | | T8 | | E | | | | T9 | | 提交 | | | | T10 | | | 读 | | ## 读已提交(RC)与MVCC * 一个事务提交之后,它做的变更才会被其他事务看到 > **每次读的时候,ReadView(一致性视图)都会重新生成** 1. 当T1时刻时,事务100修改名字为A 2. 当T2时刻时,事务100修改名字为B 3. 当T3时刻时,事务200修改名字为C 4. 当T4时刻时,事务300开始读取名字 * 此时这条数据的版本链如下 **同颜色代表是同一事务内的操作** ![](https://oncexhj.oss-cn-beijing.aliyuncs.com/image_20210109102828.png) * 来我们静下心来好好分析一下此时T4时刻事务300要读了,**究竟会读到什么数据**? 当前最近的一条数据是,C,事务200修改的,还记得我们前文说的一致性视图的几个属性吗,和按照什么规则判断这个数据能不能被当前事务读。我们就分析这个例子。 此时 (**生成一致性视图ReadView**) * **m_ids 是[100,200]:** 当前活跃的读写事务的事务id列表 * **min_trx_id 是 100:** m_ids的最小值 * **max_trx_id 是 201:** m_ids的最大值+1 当前数据的trx_id(事务id)是 200,符合min_trx_id<=trx_id<max_trx_id 此时需要判断 trx_id 是否在m_ids活跃事务列表里面,一看,活跃事务列表里面是【100,200】,只有两个事务活跃,而此时的trx_id是200,则trx_id在活跃事务列表里面,活跃事务列表代表还未提交的事务,所以该版本数据不可见,就要根据roll_point指针指向上一个版本,继续这样的判断,上一个版本事务id是100,数据是B,发现100也在活跃事务列表里面,所以不可见,继续找到上个版本,事务是100,数据是A,发现是同样的情况,继续找到上个版本,发现事务是1,数据是小杰,1小于100,trx_id<min_trx_id,代表生成这个数据的事务已经在生成ReadView前提交了,此数据可以被读到。所以读取的数据就是**小杰** 分析完第一个读,我们继续向下分析 5. 当T5时刻时,事务100提交 6. 当T6时刻时,事务300将名字改为D 7. 当T7时刻时,事务400读取当前数据 * 此时这条数据的版本链如下 ![](https://oncexhj.oss-cn-beijing.aliyuncs.com/image_20210109125236.png) 此时 (**重新生成一致性视图ReadView**) * **m_ids 是[200,300]:** 当前活跃的读写事务的事务id列表 * **min_trx_id 是 200:** m_ids的最小值 * **max_trx_id 是 301:** m_ids的最大值+1 当前数据事务id是300,数据为D,符合min_trx_id<=trx_id<max_trx_id 此时需要判断数据是否在活跃事务列表里,300在这里面,所以就是还未提交的事务就是不可见,所以就去查看上个版本的数据,上个版本事务id是200,数据是C,也在活跃事务列表里面,也不可见,继续向上个版本找,上个版本事务id是100,数据是B,100小于min_trx_id,就代表,代表生成这个数据的事务已经在生成ReadView前提交了,此数据可见,所以读取出来的数据就是**B** 分析完第二个读,我们继续向下分析 8. 当T8时刻时,事务200将名字改为E 9. 当T9时刻时,事务200提交 10. 当T10时刻时,事务300读取当前数据 * 此时这条数据的版本链如下 ![](https://oncexhj.oss-cn-beijing.aliyuncs.com/image_20210109130445.png) 此时 (**重新生成一致性视图ReadView**) * **m_ids 是[300]:** 当前活跃的读写事务的事务id列表 * **min_trx_id 是 300:** m_ids的最小值 * **max_trx_id 是 301:** m_ids的最大值+1 当前事务id是200,200<min_trx_id ,代表生成这个数据的事务已经在生成ReadView前提交了,此数据可见,所以读出的数据就是**E**. > 当隔离级别是**读已提交RC**的情况下,每次读都会**重新生成 一致性视图(ReadView)** > * T4时刻 事务300读取到的数据是**小杰** > * T7时刻 事务400读取到的数据是**B** > * T10时刻 事务300读取到的数据是**E** ## 可重复读(RR)与MVCC * 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的 >所以对于事务300来讲,它分别在T4和T10的时候,读取数据,但是它的一致性视图,**用的永远都是第一次读取时的视图**,就是T3时刻产生的一致性视图 **RR和RC的版本链是一样的,但是判断当前数据可见与否用到的一致性视图不一样** 在此**可重复读RR**隔离级别下, 1. T4时刻时事务300第一次读时的分析和结果与RC都一样,可以见上文分析与结果 2. T7时刻时事务400第一次读时的分析和结果与RC都一样,可以见上文分析与结果 3. T10时刻时事务300第二次读时的一致性视图和第一次读时的一样,所以此时到底读取到什么数据就要重新分析了 此时 (**用的是第一次读时生成的一致性视图ReadView**) * **m_ids 是[100,200]:** 当前活跃的读写事务的事务id列表 * **min_trx_id 是 100:** m_ids的最小值 * **max_trx_id 是 201:** m_ids的最大值+1 此时的版本链是 ![](https://oncexhj.oss-cn-beijing.aliyuncs.com/image_20210109130445.png) 当前数据的事务id是200,数据是E,在当前事务活跃列表里面,所以数据不可见,根据回滚指针找到上个版本,发现事务id是300,当前事务也是300,可见,所以读取的数据是**D** * **我们可以自己思考下,要是没有事务300这条更改的这条记录,又该怎么继续向下分析呢?** > 当隔离级别是**可重复读RR**的情况下,每次读都会**用第一次读取数据时生成的一致性视图(ReadView)** > * T4时刻 事务300读取到的数据是**小杰** > * T7时刻 事务400读取到的数据是**B** > * T10时刻 事务300读取到的数据是**D** ## 往期精彩推荐 * [京东这道面试题你会吗?](https://mp.weixin.qq.com/s/0kliROTioI93WjCrkRLw3w) * [?线程池为什么可以复用,我是蒙圈了。。。](https://mp.weixin.qq.com/s/uEVEfybDVXkQYA2B99caOw) * [学会了volatile,你变心了,我看到了](https://mp.weixin.qq.com/s/GSzBHDO4_f8qkzI4_B5TfA) * [mysql可以靠索引,而我只能靠打工,加油,打工人!](https://mp.weixin.qq.com/s/3tU-MzaaZN2a1c7aHe_yZg) * [你好,我叫AQS(系列一:加锁)](https://mp.weixin.qq.com/s/w1jvmldJowENrjCRKtzJ_g) ## 絮絮叨叨 如果大家觉得这篇文章对自己有一点点帮助的话,**欢迎关注此公众号**,小杰非常希望各位可以点击一下屏幕下方的 * 随手一个点击,我将无法忘记,这是我坚持创作的最大动力! * **非常欢迎** 各位号主读者一起交流学习,互相开白转发 > **若文章有误欢迎指出**,我们下篇文章见 ------------ ###### 自猿其说Logistics-JDL京东物流技术发展部 ###### 作者:中台技术部 邢焕杰 ![](//img1.jcloudcs.com/developer.jdcloud.com/b5e16561-e713-4e64-a56a-8f474afeaaf020210209102811.png)
原创文章,需联系作者,授权转载
上一篇:别困惑,不是你的错!90%的开发者把Clubhouse看成了Clickhouse!
下一篇:京东App Swift 混编及组件化落地
相关文章
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专业服务
扫码关注
京东云开发者公众号