MySQL read view 在RR和RC隔离级别下的异同

1.首先了解下什么是read view

这里说的 read view 是InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。 
read view 并没有物理结构,作用是事务执行期间用来定义"我能看到什么数据"。

2.事务id

InnoDB 里面每个事务有一个唯一的事务 ID,叫做 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。 
在innodb存储引擎下,聚簇索引记录中都包含两个必要的隐藏列: 
trx_id:每次对某条记录进行改动时,对会把对应的事务id赋值给trx_id隐藏列; 
roll_pointer:每次对某条记录进行改动时,这个隐藏列会存一个指针,可以通过这个指针找到该记录修改前的信息,也就是undo回滚段中的内容。

3.RR(Repeatable Read,可重复读)隔离级别下的实现

在可重复读隔离级别下,事务在启动的时候就"拍了个快照"。注意,这个快照是基于整库的。 
以一个事务启动的时刻为准,如果一个数据版本是在这个事务启动之前生成的,就可以看到;如果是在这个事务启动以后才生成的,就看不到,就必须要找到它的上一个版本。 
实现上,事务启动的瞬间,InnoDB 为这个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在"活跃"的所有事务 ID。“活跃”指的就是,启动了但还没提交。数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。这个视图数组和高水位,就组成了当前事务的一致性视图(read-view),不同时刻启动的事务会有不同的 read-view。

而每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。 
这个read view 视图数组把所有的 row trx_id 分成了几种不同的情况:已提交事务、未提交事务集合、未开始事务。当前事务一定在低水位和高水位之间。

 

那么总结下来可以有几个概念就是: 

m_ids:表示在生成readview时,当前系统中活跃的读写事务id数组; 
min_trx_id:表示在生成readview时,当前系统中活跃的读写事务中最小的事务id,也就是m_ids中最小的值,就是低水位; 
max_trx_id:表示生成readview时,系统中应该分配给下一个事务的id值,就是高水位; 
creator_trx_id:表示生成该readview的事务的事务id;

而数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的: 
1.如果被访问版本的row trx_id,与readview中的creator_trx_id值相同,表明当前事务在访问自己修改过的记录,该版本可以被当前事务访问; 
2.如果被访问版本的row trx_id,小于readview中的min_trx_id值,表明生成该版本的事务在当前事务生成readview前已经提交,该版本可以被当前事务访问; 
3.如果被访问版本的row trx_id,大于或等于readview中的max_trx_id值,表明生成该版本的事务在当前事务生成readview后才开启,该版本不可以被当前事务访问; 
4.如果被访问版本的row trx_id,值在readview的min_trx_id和max_trx_id之间,就需要判断trx_id属性值是不是在m_ids列表中, 
如果在:说明创建readview时生成该版本的事务还是活跃的,该版本不可以被访问; 
如果不在:说明创建readview时生成该版本的事务已经被提交,该版本可以被访问。

现有示例如下: 

那么 事务1、事务2、事务4的查询结果是多少呢? 
事务1的两次查询结果都是 100 
事务2第一次查询是100,第二次查询是80 
事务4的第一次查询是90

这个结果是不是同你得出的结果一致呢?现在我们来解析例子中的结果: 
start transaction with consistent snapshot; 这条语句执行完就是开启了一个事务,按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。

假设在事务1之前有一个活跃事务,这个事务id是99;事务1、事务2、事务3、事务4的事务id分别是100,101,102,103;这个例子的期间没有其他事务;假设这行数据在这三个事务开始之前是row trx_id是90; 
那么: 
事务1启动的时候,read view数组中的值是[99,100] 
事务2启动的时候,read view数组中的值是[99,100,101] 
事务3启动的时候,read view数组中的值是[99,100,101,102] 
事务4启动的时候,事务3已经提交了,所以read view数组中的值是[99,100,101,103]

数据 id = 1 这条记录的数据版本如下: 

从图中可以看出,第一次有效更新是事务2把(id,score) 从 (1,100)更新成了(1,90),那么此时这个数据的最新版本就变成了row trx_id 102,而trx_id 90 这个版本就成了历史版本;第二次有效更新是事务2把(id,score)从当前值(1,90)更新成了(1,80),这个数据的最新版本变成了 row trx_id 101,而trx_id 102成为了历史版本。

对于事务1来说,101就是高水位,活跃事务有[99,100],在第一次查询的时候,id=1的数据的row trx_id是90,小于read view中的最小值即低水位值99,所以此时的数据是可见的;第二次查询的时候,id=1的数据的row trx_id是101,等于read view中的高水位,数据不可见,继续往前找数据的历史版本,发现历史版本1的row trx_id是102,大于read view中的高水位,数据依然不可见,再往前找历史版本2的row trx_id是90,小于read view中的最小值即低水位值99,所以此时的数据是可见的;所以事务1两次的查询id=1的值都是100

对于事务2来说,102是高水位,活跃事务有[99,100,101],在第一次查询的时候,id=1的数据的row trx_id是102,等于read view中的高水位,数据不可见,继续往前找数据的历史版本2 row trx_id是90,小于read view中的最小值即低水位值99,所以此时的数据是可见的,第一次查询id=1结果是score=100;第二次查询的时候,id=1的数据的row trx_id是101,等于自己当前的事务id,就是说现在查询的数据的更改是自己更改的,所以查询id=1结果是score=80

事务3对数据进行了修改就提交了,对数据的修改是当前读。

对于事务4来说,104是高水位,活跃事务有[99,100,101,103],查询的时候,id=1的数据的row trx_id是101,101介于低水位和高水位之间(介于readview的min_trx_id和max_trx_id之间)且就在活跃的事务列表中,说明此时事务还没有提交,是不可见的,否则就是脏读,继续往前发现历史版本1的row trx_id是102,102介于低水位和高水位之间,但是不在活跃的事务列表中,说明此时事务已经提交,数据是可见的,所以查询id=1的结果是90

4.RC(Read Committed,读提交)隔离级别下的实现

在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。 
在读提交隔离级别下,"start transaction with consistent snapshot;" 就等于 start transaction,start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。 
所以示例在RC隔离级别下:

 

事务1的第一次查询结果是100,第二次查询结果是90 
事务2第一次查询是90,第二次查询是80 
事务4的第一次查询是90

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章