mysql RR隔离级别下事务特性小结,通过学习了林晓斌的实战45讲的一些理解:
我们知道如果是可重复读隔离级别,事务 T 启动的时候会创建一个视图 read-view(其实就是一批trx_id号),之后事务 T 执行期间,即使有其他事务修改了数据,事务 T 看到的仍然跟在启动时看到的一样。也就是说,一个在可重复读隔离级别下执行的事务,好像与世无争,不受外界影响。
并且一个事务要更新一行,如果刚好有另外一个事务拥有这一行的行锁,它又不能这么超然了,会被锁住,进入等待状态。问题是,既然进入了等待状态,那么等到这个事务自己获取到行锁要更新数据的时候,它读到的值又是什么呢?
关于如何实现RR隔离级别下的一致性读??
在 MySQL 里,有两个“视图”的概念:
一个是 view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view … ,而它的查询方法与表一样。
另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”
“快照”在 MVCC 里是怎么工作的?
在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。
这时,你会说这看上去不太现实啊。如果一个库有 100G,那么我启动一个事务,MySQL 就要拷贝 100G 的数据出来,这个过程得多慢啊。可是,我平时的事务执行起来很快啊。
实际上,我们并不需要拷贝出这 100G 的数据。我们先来看看这个快照是怎么实现的。
InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。
也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。
如图 2 所示,就是一个记录被多个事务连续更新后的状态。

图 2 行状态变更图
图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。
你可能会问,前面的文章不是说,语句更新会生成 undo log(回滚日志)吗?那么,undo log 在哪呢?
实际上,图 2 中的三个虚线箭头,就是 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 分成了几种不同的情况。

图 3 数据版本可见性规则
这样,对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:
如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
如果落在黄色部分,那就包括两种情况
a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
比如,对于图 2 中的数据来说,如果有一个事务,它的低水位是 18,那么当它访问这一行数据时,就会从 V4 通过 U3 计算出 V3,所以在它看来,这一行的值是 11。
原理:1)前面讲到的数据库的多版本控制mvcc,然后InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。
数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view),RR或者RC实现的事务隔离性,都是借助这个功能实现的!最终通过对比当前事务的一致性视图来判断是不是可见!
注意理解:
1、这里的高水位线是系统已经创建的事务id的最大值加1,为什么是系统创建的id的最大值,而不是活跃数组中的最大值?因为活跃数组中的最大trx_id <= 系统最大trx_id,但是我们需要用这个最大trx_id+1的值代表未来要产生的事务id,所以需要用系统最大trx_id而不是活跃数组中的最大trx_id;
2、为什么需要+1?因为当前最大的trx_id有可能已经提交了,那么对当前事务来说是可见的,但是系统最大trx_id+1就代表了一定是未来产生的,一定是不可见的!
3、为啥一致性视图的最小trx_id是活跃数组的最小trx_id?因为我们需要一个trx_id,只要小于这个trx_id的版本都是可见的。然后活跃数组的最小trx_id就能扮演这个角色!因为活跃数组就是当前正在执行但是还没有提交的活跃的事务的集合,那么对于该事务来说,这个数组中包含了所有已经启动但是未提交而不可见的事务的trx_id,自然包含不可见版本trx_id的最小值了!所以就用它为一致性视图的最小trx_id即可!
解读分析:
这个活跃数组是关键,活跃数组是判断那个版本数据可见的关键,也就是实现rr隔离级别下一致性视图的关键,并且注意不一定说事务trx_id小就一定可见,事务trx_id比当前事务trx_id小,但是由于小的事务还未提交,它仍然不可见;
RR解决幻读的原理:RR隔离级别是在事务开启的时候创建一致性视图,并在这个事务内一直依据这个一致性视图,而RC是在每个语句执行的时候创建一个一致性视图;所以RR隔离级别下同一个事务内看到的是一致性的,解决了幻读;RC隔离级别下同一个事务看到的可能不一样,会出现幻读,
1)同一个事务下,从发出第一个query开始,之后查询到的数据是一样的,借助的是MVCC多版本控制(之前的版本并不是真实物理存在的),多版本控制是通过undo来计算出来的;
2)undo log 是在ibdata1共享表空间中,如果遇到长事务,可能会导致产生很多undo,由于这些undo不能被覆盖,所以可能导致ibdata1暴涨(mysql 5.7支持在线收缩undo表空间),但是mysql应该不会出现 oracle ora-01555的问题;
3)针对RR隔离级别下,查询是在一个事务里查询到的是一样的,但是关于dml操作,给查询有点区别,dml操作的时候,是以最新的版本为基础的,也就是说如下事务c更新后自动提交,事务B接下来更新是以事务c更新后的数据为基础的,同理delete 和insert也是如此!如下图所示,如果k的开始值是1,那么事务B中的查询到的值是3!
![]()