事务隔离级别的简介与MVCC的实现

Posted 刘俊岐.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了事务隔离级别的简介与MVCC的实现相关的知识,希望对你有一定的参考价值。

大家对mysql事物的隔离级别一定很清楚,分别有:

  1. 读未提交
  2. 读已提交
  3. 可重复读
  4. 串行化

而mysql默认的隔离级别是可重复读,那么今天我就给大家说说可重复读的实现原理。

可重复的的意思就是在在一个事务开始过后,无论是否其他事务对某一行或某几行进行了修改,只要本事务没有对其修改,那么这些行的数据在本事务中永远是不变的。所以在mysql内部究竟是如何实现这一机制的呢?实现这一机制是否会带来其他的问题呢?

首先我们关注下这个事务执行的时序图

事务A事务B
开启事务开启事务
查询得到值C
查询的到值C
将值C改为值D
查询得到值E
提交事务B
查询得到值F
提交事务A

我们可以看下,当事务的隔离级别分别是读未提交,读已提交,可重复读时候值A、值B、值D究竟是多少。

  • 当事务隔离级别是读未提交的时候,事务B执行修改值C值为D后,事务A可以立即看到,所以值E和值F都等于值D
  • 当事务隔离级别是读已提交的时候,当事务B未提交的时候,我们还看不到值D,所以值E等于值C,直到事务B提交之后,事务A再次查询的时候,会看到修改后的值D
  • 当事务隔离级别是可重复读的时候,事务A自始至终都看不到事务B对值C的修改,所以值E和值F都等于值C

可是数据库是如何实现这种机制的呢?在数据库里面,会创建一个视图,取值的时候视图的逻辑结果为准,当隔离级别是可重复读时候,这个视图是开启事务时创建的,而当隔离级别是读已提交的时候,这个视图是在每个sql语句提交的时候创建的。

我们可以通过参数transaction-isolation设置mysql的隔离级别

事务隔离级别的具体实现

这里我详细说明下可重复读的实现原理,为了支持事务的回滚操作,在mysql中设置了undo log,实际上在每一条记录更新的时候都会记录一个回滚的操作,记录上的最新值都可以通过回滚操作获得之前状态的值。那么undo log中记录的到底是什么呢?下面我举一个例子,将一个值从1依次改到2、3、4后,mysql中的undo log是什么样子的。

image-20210507162524193

当我们在修改这个记录的同时,不同时刻开启的事务就会看到不同的read-view,也就是说同样的一条记录在多个事务中可能有多个不同的版本,这也就是MVCC的原理。

也许你会发现如果这样下去,我们的undo log会越来越长,这样下去肯定不行。在InnoDB的设计中,等到这些undo log不需要的时候会进行删除,那怎么判断什么时候是不需要呢?答案就是当前系统里没有比这个回滚日志更早的read-view的时候

这是我们会想到,如果我们使用了一个很长的事务,这时这个事务的undo log会非常大,占用很大的磁盘空间,所以我们尽量也不要使用长事务。

MVCC的实现

既然提到了刚刚提到了MVCC那我们就不得不详细说明下。

MVCC(多版本并发控制)存在的目的就是对数据库的并发访问提供一种实现方式。

在我们刚刚说到的可重复读隔离级别中,就是在事务启动的时为整个库拍了一个“快照”,但是这个快照肯定不是拷贝数据得来的,如果是拷贝数据的话,那得多慢呀!MySQL肯定不会使用这种方式,那这个“快照”在MVCC下是怎么工作的呢?

在InnoDB里面,每个事物启动的时候,InnoDB都会赋给其一个事务ID(transation id)。同时每行数据都有其数据的版本(row trx_id),而这个数据的版本就是更新这行数据事务的版本,并且老的数据会保留,并且会有方法拿到他(也就是通过上述的undo log)。

下图表示了undo log和每行记录的关系

image-20210507170815488

弄清楚了row trx_id 和 transation id 以及undo log的概念以及他们之间的关系后,我们再来说下InnoDB怎样定义哪些数据在当前的事务中是可见的。也就是如何实现可重复读中的要求:以当前事务启动的时刻为准,一个数据版本在当前事务启动之前生成的就可见,否则就不可见,我需要找到其上一个版本。

在MySQL内部的实现上,在每一个事务启动的时候,会为每一个事务构建一个数组,来保存当前启动了还没提交的事务。同时在这个数组中,事务ID的最小值就记为低水位,事务ID的最大值+1就记为高水位

image-20210507171926965

所以,规则如下:

  1. 如果一个数据的版本落在绿色部分,可见
  2. 如果一个数据落在黄色部分,分两组情况
    1. 这个数据的row trx_id在数组中,不可见
    2. 这个数据的row trx_id不在数组中,可见
  3. 如果数据落在红色部分,不可见

所以,当前事务可见的值不是最新的数据时,就需要通过row trx_id配合undo log还原当前事务快照的值。这样也就实现了MySQL秒级创建快照的能力。

但是对于更新过程可能会有点区别,我们看下下面这个时序图

序号事务A事务B
T1begin;
T2select c from T where id = 1
T3update T set c = c+1 where id = 1
T4update T set c = c+1 where id = 1
T5select c from T where id = 1

假设id=1这行c的记录初始值为0,那么执行事务A T5的返回值应该是多少呢。

答案是2,这是因为update语句是当前读,不是快照读,要获取当前行数据最大的row trx_id所对应的结果

以上是关于事务隔离级别的简介与MVCC的实现的主要内容,如果未能解决你的问题,请参考以下文章

Mysql—4种隔离级别以及MVCC一致性视图的实现原理

MySQL里的undo log, MVCC以及事务隔离级别

MYSQL的事务隔离级别,MVCC,readView和版本链小结

事务隔离级别与MVCC

性能优化|MVCC通俗理解与事务隔离级别实战操作

分布式事务MVCC事务隔离级别