暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL read view 在RR和RC隔离级别下有什么不同

学徒杨新建 2021-06-28
562

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


文章转载自学徒杨新建,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论