如果早知道MVCC可以这样学,我也不至于被面试官虐的这么惨!

什么是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,则返回此条数据。

注意:在所有查找过程中,匹配到最终可见的数据后,还需要判断数据的删除标记为是否已经标记为删除状态,如果标记为删除状态,则不返回此条数据,并且终止向下查询!!!

Java高级架构面试知识点

我整理的《最全Java高级架构面试知识点整理》已升级为2.0版本,200个知识点,178页。点击这获取。

你可能感兴趣的:(mysql,java,数据库,python,面试)