什么是MVCC?
MVCC是Multi-Version Concurrency Control(多版本并发控制)的缩写。
MVCC解决了什么问题?
我们知道在mysql中有四种事务隔离级别:读未提交、读已提交、可重复读和串行读。
在四种隔离级别中,可重复读就是通过MVCC实现的。通过MVCC,能够保证在事务开启后,保证每次读取的数据都是一样的;但是却不能解决幻读的问题,庆幸的是mysql使用间隙锁解决了在可重复读级别下出现的幻读问题。
MVCC实现原理
MVCC主要是借助mysql的undo log和一致性视图(快照)来实现。
undo log 记录了数据在变迁过程中所关联的事务ID;
一致性视图(快照)保存了线程在开启一个事务之后,数据的一个快照点,记录当前事务的状态。
那么MVCC是如何通过undolog 和一致性视图来实现可重复读的呢?
首先我们思考这样一个问题,在可重复读模式下,开启一个事务之后会是什么样的场景:
- 能看到本事务开启前的所有已经提交的事务产生的数据
- 不能看到未提交的事务产生的数据
假设每个事务都有自己的事务ID,并且这个id是递增的,后创建的事务ID大于先创建的事务ID
所以如果想要实现这样一个场景,开启事务后,需要保存以下两个数据状态:
- 未提交的事务作为一个数组 un_commit[],按顺序排列
- 生成一个下一个即将分配的事务ID MAX_ID
准备工作做好以后,我们先介绍下mysql在新增、删除和修改数据的时候,mysql底层是如何存储的
mysql 如何记录我们增删改的数据?
mysql在底层为undolog 中每条数据都会增加三个伪字段字段:创建事务ID,是否删除标记(默认否),上一版本指针
数据记录是按照数据更新时间从上往下排的,这里为了书写方便,更换了排列顺序,请注意区分
- 初始结构
Id | Name | txc_id | 是否删除 | 上一版本指针 |
1 | yang | 100 | False | 空 |
- 修改 name= zhang
Id | Name | txc_id | 是否删除 | 上一版本指针 |
1 | yang | 100 | False | 空 |
1 | zhang | 200 | False | 地址1 |
- 新增id=2
Id | Name | txc_id | 是否删除 | 上一版本指针 |
1 | yang | 100 | False | 空 |
1 | zhang | 200 | False | 地址1 |
2 | lisi | 300 | False | 空 |
- 删除id=2
Id | Name | txc_id | 是否删除 | 上一版本指针 |
1 | yang | 100 | False | 空 |
1 | zhang | 200 | False | 地址1 |
2 | lisi | 300 | False | 空 |
2 | lisi | 400 | true | 地址2 |
不管新增删除还是修改,都是复制一份数据,而不是在原有数据上操作,这样最终就会形成一个数据链,很适合做快照。
通过上面的描述,大家应该对mysql如何通过undolog存储我们的数据链有了一个大概的认识,现在我们回归正题:MVCC是如何通过undolog 来查找我们的数据,实现可重复读呢?
MVCC是如何查询我们想要的数据,保证可重复读呢?
在前面已经提到过,mysql在开启事务后,会生成一个一致性视图,其实对于程序来说就是记录当前的数据点:
- 未提交的事务做一个数组 un_commit[],按顺序排列
- 生成一个下次即将分配的事务ID MAX_ID
ok,现在我们利用这两组数据,来查找id为1的数据
假设当前分配的事务ID为300,目前有两个未提交的事务[100,200],我们现在模拟下查找流程
初始状态
Id | Name | txc_id | 是否删除 | 上一版本指针 |
1 | yang | 50 | False | 空 |
A开启事务后,第一次查找
执行了第一条select语句时,系统分配了一个事务ID 300,此时有两个未提交的事务100,200,目前是想要查找id为1的记录
- 比较第一条,提取创建事务id=50,比较后发现创建事务id小于当前事务ID=300,进入下一步
- 判断 创建事务id小于最小的未提交事务id=100,则可以认为当前这条数据是在本事务开启之前就已经提交了,所以返回此条数据。
- 查找完成
此时事务ID=100的修改了id=1 的数据,并且提交了事务
此时的数据长这样:
Id | Name | txc_id | 是否删除 | 上一版本指针 |
1 | zhang | 100 | False | 地址1 |
1 | yang | 50 | False | 空 |
A事务此时进行第二次查找
从上往下找
- 提取第一条数据,判断发现创建事务id=100是小于当前事务ID=300,则进入下一个判断
- 判断发现事务id=100 是在 未提交数组[100,200]中,所以对当前事务事务是不可见的,进入下一个判断
- 提取上一个版本指针的地址,定位到数据
- 比较发现当前数据创建的事务id是50,小于最小的未提交事务的id,所以返回此条数据
此时事务ID=200的删除了id=1 的数据,没有提交事务
此时数据长这样:
Id | Name | txc_id | 是否删除 | 上一版本指针 |
1 | zhang | 200 | true | 地址2 |
1 | zhang | 100 | False | 地址1 |
1 | yang | 50 | False | 空 |
A事务此时进行第二次查找
此次查找过程和上面一样,最终定位到事务id=50时产生的数据记录
A事务进行了update操作后,会更新数据视图
未提交数组:[200],当前预分配的事务ID=400
A开启事务后,进行第一次查询
生成数据视图保存点:
未提交数组:[200],当前预分配的事务ID=400
- 从第一条开始比较,发现事务Id=200在 未提交事务的数组中,则根据地址2找到下面一条记录
- 创建事务id=100 小于最小的未提交事务id=200,则返回此条数据。
注意:在所有查找过程中,匹配到最终可见的数据后,还需要判断数据的删除标记为是否已经标记为删除状态,如果标记为删除状态,则不返回此条数据,并且终止向下查询!!!
公众号“AI码师”