深入浅出MySQL事务(二)MVCC的实现原理

  • Post author:
  • Post category:mysql




深入浅出MySQL事务(二)MVCC的实现原理

上一篇文章介绍了事务隔离的实现,里面讲到,事务通过创建一个视图,然后根据视图逻辑来获取当前事务看到的数据,你可能会好奇这个视图是何方神圣,本文就来好好讲讲视图的实现原理。



一、视图是如何创建的?

在InnoDB里面,每个事务都有一个ID,叫做 transaction id,这个事务ID是在事务启动时向事务系统申请的,按照顺序严格递增进行分配。

数据表中的每个数据行隐藏着一个列,这个列是用来记录更新它的事务的事务ID,这里暂且把它称为 row trx_id,那么,每个数据行的每个版本都会对应一个 row trx_id,如下所示。

在这里插入图片描述

需要注意一点,上述的每个版本的数据实际上是不存在的,只存在当前最新版本,其他版本都是通过回滚日志来获取的,如上述的u1、u2、u3操作。所以真正存在的不是v1、v2、v3,而是u1、u2、u3这些回滚日志。

明白了多版本和事务ID的关系后,我们下面来将视图是如何创建的。



可重复读

的隔离级别举例,在事务的时刻,会创建一个数组,这个数组保存着当前活跃的事务的事务ID,所谓活跃的事务指的是正在运行但还未提交的事务。

将数组中事务ID最小值计为低水位,最大值加1记为高水位。

这个数组和高低水位就组成了当前事务的

一致性视图

而数据版本的可见性就是由这个一致性视图和数据版本的 row trx_id 来进行比较判断的。

到此视图的神秘面纱已经被揭开了,下面来讲讲如何使用这个一致性视图来判断数据版本的可见性的。



二、查询逻辑

上述说的低水位和高水位可以将所有的事务ID分为3部分,如下所示。

在这里插入图片描述

对于当前事务启动瞬间,数据版本的 row trx_id 可能有以下几种情况

  • 落在绿色区域:表示这个版本是已提交的事务或者当前事务修改的数据,对当前事务是可见的。
  • 落在红色区域:表示这个版本是将来启动的事务修改的,对当前事务是不可见的。
  • 落在黄色区域

    • 如果 row trx_id 存在视图数组中,表示这个版本是由未提交的事务修改的,对当前事务是不可见的。
    • 如果 row tax_id 不在视图数组中,表示这个版本是由已提交的事务修改的,对当前事务是可见的。

下面举个例子

首先创建一张表并插入一行数据

create table T(
	id int,
) engine=innodb;

insert into T(id) value(1);

按照下面的顺序来执行两个事务,其中 start transaction with consistent snapshot表示立即启动一个事务(如果是没有加上 with consistent snapshot 要等到执行第一个sql语句才会启动事务)。

在这里插入图片描述

假设事务的隔离级别是

可重复读

,如果你阅读过上一篇文章,你应该知道T1、T2、T3事务A读取的id都等于1,下面我们来讲一下这其中的实现过程。

为了简化这个过程,我们不妨做以下假设

  • 系统中现在只有事务A、事务B、事务C
  • 事务A的事务ID等于3
  • 事务B的事务ID等于7
  • 事务C的事务ID等于5
  • 刚开始id=1这个数据行的row trx_id 等于1

我们从事务C的视角来分析这一整过程

  1. 在事务C启动事务的时候,会创建视图。此时活跃的事务是事务A和事务C,事务ID分别为3和5,所以是视图数组为[3,5],低水位为3,高水位为6,如下所示。

    在这里插入图片描述

  2. T1时刻,此时数据还未被修改,其形式如下。

    在这里插入图片描述

    此版本数据的 row trx_id 等于1,小于事务C的视图数组的低水位,落在绿色部分,所以它对事务C是可见的,因此事务C得到 id=1。

  3. 之后事务A对数据进行修改,数据就会变成下面这样子

    在这里插入图片描述

  4. T2时刻,事务C在进行读取,当前数据的最新版本为v2,它的 row trx_id=3,落在事务C视图数组的黄色部分,3存存在于数组中,因为判断这个版本是由未提交的事务修改的,对当前事务C是不可见的。

    然后根据回滚操作u1,获取到v1版本的数据,该版本的 row trx_id=1,落在绿色区域,事务C可见,所以事务C读到 id=1。

  5. 接下来事务B对数据进行修改,需要注意的是更新操作都会在当前最新版本上面进行,下面讲更新逻辑的时候再详细讨论,进过事务B的操作后,数据行就变成下面这样。

    在这里插入图片描述

  6. T3时刻,事务C读区数据,发现当前最新版本为v3,它的 row trx_id=7,落在红色区域,所以该版本数据对事务C是不可见的。因此通过u2回滚得到v2版本,发现改版本数据对事务C也是不可见的。再通过u1回滚到v1版本,最后判断v1版本对事务C是可见的,因此事务C读到的 id=1。

这就是视图查询逻辑的工作过程,下面来看看更新逻辑。



三、更新逻辑

我们先来看看下面这个例子。

首先创建一张表并插入一行数据。

create table T(
	id int,
) engine=innodb;

insert into T(id) value(1);

按下面顺序执行两个事务。

在这里插入图片描述

事务的隔离级别依旧是

可重复读

你可以先思考下事务A在T1和T2时刻读取到的id是多少。

如果我告诉你T1时刻id=1,T2时刻id=3,你会不会很惊讶。不是说好的可重复读吗,T2时刻id不应该等于2吗?

其实更新操作总是先执行读,再修改,这里读是

当前读

,也就是读取当前最新的版本。

所以当事务A修改的时候,当前最新版本如下。

在这里插入图片描述

当事务A再对其进行修改的时候,会直接在当前最新版本上进行修改,就会变成下面这样子。

在这里插入图片描述

当T2时刻事务A再进行读取的时候,当前最新版本的 row trx_id=3,判断它等于当前事务的事务ID,应该对当前事务可见,得到 id=3。

以上就是更新逻辑。

除了update语句更新数据会使用

当前读

之外,select语句查询时加锁也会导致当前读,如下sql语句。

mysql> select id from T where id=1 lock in share mode; // 共享锁
mysql> select id from T where id=1 for update; // 排他锁

最后讲一下,我们上面的例子都是使用

可重复读

的隔离级别,其实使用

读提交

的隔离级别原理也是一样的,两者的区别在于

读提交

的隔离级别在每次执行sql语句的时候都会新创建一个视图,这里就不详细举例说明了。



四、小结

本文介绍了视图的实现细节,在

可重复读

的隔离级别下,事务启动时会创建一个视图数组,得到相应的高低水位。在查询逻辑下,使用视图数组判断版本数据是否可见,如果不可见,则利用回滚日志得到上一版本的数据,然后再判断是否可见。在更新逻辑下,会使用当前读,例外在select加锁的情况下也会使用当前读。



版权声明:本文为weixin_42462202原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。