深入浅出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的视角来分析这一整过程
-
在事务C启动事务的时候,会创建视图。此时活跃的事务是事务A和事务C,事务ID分别为3和5,所以是视图数组为[3,5],低水位为3,高水位为6,如下所示。
-
T1时刻,此时数据还未被修改,其形式如下。
此版本数据的 row trx_id 等于1,小于事务C的视图数组的低水位,落在绿色部分,所以它对事务C是可见的,因此事务C得到 id=1。
-
之后事务A对数据进行修改,数据就会变成下面这样子
-
T2时刻,事务C在进行读取,当前数据的最新版本为v2,它的 row trx_id=3,落在事务C视图数组的黄色部分,3存存在于数组中,因为判断这个版本是由未提交的事务修改的,对当前事务C是不可见的。
然后根据回滚操作u1,获取到v1版本的数据,该版本的 row trx_id=1,落在绿色区域,事务C可见,所以事务C读到 id=1。
-
接下来事务B对数据进行修改,需要注意的是更新操作都会在当前最新版本上面进行,下面讲更新逻辑的时候再详细讨论,进过事务B的操作后,数据行就变成下面这样。
-
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加锁的情况下也会使用当前读。