1.前言
在可重复读隔离级别下,事务T启动的时候会创建一个视图read-view,之后事务T执行期间,即使有其他事务修改了数据,事务T看到的仍然跟在启动时看到的一样。也就是说,一个在可重复隔离下执行的事务,好像与世无争,不受外接影响。
2.看到的到底是什么?
但是,在上一篇分享行锁的时候又提到,一个事务要更新一行,如果刚好有另外一个事务拥有这一行的行锁,它又不能这么超然了,会被锁住,进入等待状态。问题是,既然进入等待状态,那么等到这个事务自己获取到行锁要更新数据的时候,它读到的值又是什么呢?
下面是一个只要两行的表的初始化的语句:
mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL, `k` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; insert into t(id, k) values(1,1),(2,2);
这里,我们需要注意的是事务的启动时机
begin/start transaction命令并不是一个事务的起点,在执行到它们之后的第一个操作innodb表的语句,事务才真正启动,如果你想要马上启动一个事务,可以使用start transaction with consistent snashot这个命令
1.第一种启动方式,一致性视图是在执行第一个快照读语句时创建的;
2.第二种启动方式,一致性视图是在执行start transaction with consistent snapshot时创建的
在这个例子中,事务c没有显示地使用begin/commit,表示这个update语句本身就是一个事务,语句完成的时候会自动提交。事务B在更新了行之后查询;事务A在一个只读事务中查询,并且时间顺序上是在事务B的查询之后。
这时,你就会在事务B查到的k的值是3,而事务A查到的k的值是1
在Mysql里,有两个“视图”的概念:
- 一个是view.它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果,创建视图的语法是create view...,而它的查询方法与表一样。
- 另一个是innodb在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC和RR隔离级别的实现。
3.“快照”在MVCC里是怎样工作的?
在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。
这时,你会说这看上去不太现实啊。如果一个库有100G,那么我启动一个事务,Mysql就要拷贝100G的数据出来,这个过程得多慢啊,可是,我平时的事务执行起来很快啊。
实际上,我们并不需要拷贝出这100G数据。我们先来看看这个快照是怎样实现的。
innodb里面每个事务有一个唯一的事务ID,叫作transaction id。它是在事务开始的时候向innodb的事务系统申请的,是按申请顺序严格递增的。
而每行数据也都是有多个版本的,每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务id,记为row trx_id.同时,旧的数据版本要保留。并且在新的数据版本中,能够有信息可以直接拿到它。
也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id.
如下图所示,就是一个记录被多个事务连续更新后的状态。
图中虚线框里是同一行数据的4个版本,当前最新版本是v4,k的值是22,它是被transaction id为25的事务更新的,因此它的row trx_id也就是25.
你可能会问,前面的文章不是说,语句更新会生成undo log(回滚日志)吗?那么,undo log在哪儿呢?
实际上,上图的三个虚线箭头,就是undo log;而v1、v2、v3并不是物理上真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的,比如需要v2的时候,就是通过v4依次执行u3、u2算出来。
明白了多版本和row trx_id的概念后,我们再来想想一下,innodb是怎么定义那个’100G'的快照的。
按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果,但是之后,这个事务执行期间,其他事务的更新对它不可见。
因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本。”
当然,如果“上一个版本”也不可见,那就的继续往前找。还有,如果是这个事务自己更新的事务,它自己还是要人的。
在实现上,innodb为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务id."活跃“指的就是,启动了还没有提交。
数组里面事务id的最小值记为低水位,当前系统里面已经创建过的事务id的最大值加1记为高水位。
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view).
而数据版本的可见性规则,就是基于数据的row trx_id和这个一致性视图的对比结果得到的。
这个视图数据把所有的row trx_id分成了几种不同的情况。
这样,对于当前事务的启动瞬间来说,一个数据版本的row trx_id,有以下几种可能:
1.如果落在绿色部分,表示这个版本是已经提交的事务或者是当前事务自己生成的,这个数据是可见的;
2.如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
3.如果落在黄色部分,那就包括两种情况
a.若row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;
b.若row trx_id不在数据中,表示这个版本是已经提交了的事务生成的,可见;
比如,对于第一幅图的数据来说,如果有一个事务,它的低水位是18,那么当它访问这一行数据时,就会从v4通过U3计算出v3,所以在它看来,这一行的值是11.