读已提交和可重复读隔离级别下到底能读到哪些记录?

事务的隔离性及MySQL多版本并发控制实现这篇文章中讲过中讲过,在RC和RR隔离级别下MySQL有ReadView来实现隔离级别。​

在RC隔离级别下,ReadView是在每次执行查询时会生成;
在RR隔离级别下,当使用start transaction with consistent snapshot开启事务,那ReadView就是在开启事务的时候创建,如果使用begin或者start transaction,ReadView就是在开启事务后执行第一条查询语句时才会生成。


事务ID(trx_id)

InnoDB中每开启一个事务,都会生成一个事务id(自增的),这里就需要注意,事务id的生成和ReadView的生成不是同一时刻(RC和RR都一样)。

如果事务更新了一行记录,那么这行记录会绑定上当前事务id,也就是说一行记录如果被多个事务更新过,那这行记录实际上会有N个版本,每个版本都对应一个事务id,这个ID叫row_trx_id。

举例:
假设数据库的隔离级别为RR,目前有一行记录id=1,c=1,被事务A、B、C依次更新c=c+1,那对应的版本就是:

  1. V1:1,1
  2. V2:1,2(事务A)
  3. V3:1,3(事务B)
  4. V4:1,4(事务C)

假设在事务A之前有事务O在执行,已经创建了ReadView,那事务O在C更新完之后再去查询,需要保证读到的是V1版本,其实不是直接读ReadView,而是通过回滚日志来还原出这一行自己能读到的版本,也就是V1,通过V4->V3->V2->V1这样一步一步还原出来。
这也就表明ReadView不是物理视图,而是逻辑视图。

事务数组

InnoDB在事务执行非当前读(当前读下边会将)查询时会为这个事务构造一个数组,数组中保存当前所有未提交的事务ID,注意,这里数组中会出现比自己事务ID大的事务ID,因为数组不是在生成事务ID后立马就创建的,可能是过了一段时间(因为执行非当前读查询时才会构造这个数组,前边已经讲过了,怕小伙伴有疑惑就再啰嗦一遍😐)。

高低水位

低水位是最小的未提交的事务ID(也就是事务数组中的最小的事务ID),注意,高水位是执行查询时当前系统里记录过的最大事务ID+1,
注意: 高水位可不是事务数组中最大的事务ID哦😐.

那在事务执行查询的时候,读到的记录行的事务ID就这么几种情况:
在这里插入图片描述

绿->蓝->红,事务ID依次递增

  • row_trx_id在绿色区域时,比事务数组中低水位的ID还小,说明事务已经提交了,该行版本可见
  • row_trx_id在蓝色区域时,row_trx_id比事务数组中的低水位大,就去看下事务数组中有没有这个事务ID
    • 如果有就说明这个事务的提交是在在生成ReadView之后,因此不可见。
    • 如果没有就说明这个事务的提交是在生成ReadView之前,因此可见。
  • 当row_trx_id在红色区域时,事务ID大于高水位,表示生成ReadView的时候这个事务还没创建,肯定是不可见的。

通过“高低水位”和“事务数组”就可以判断出一个数据版本是否可读

举例:
事务A的ID为99,事务A执行查询时所有未提交的事务ID有[97,99,100,103],系统中最大事务ID为106,此时事务A的事务数组中就有这三个事务ID,高低水位情况如下:
在这里插入图片描述


假设在读id=1这行记录的所有的row_trx_id版本为[98,100,102,103],此时事务A要读id=1这一行数据,就要先判断row_trx_id这个版本是否可见,trxId=103这个事务在自己的事务数组中,说明自己的一致性视图创建时这个事务还没有提交,所以不可见;因此去读上一个版本row_trx_id=102,102处于蓝色区域,并且不在自己的事务数组中,说明这个事务是在自己一致性视图创建之前就提交了,因此102是可见的,就读出来102的版本。

总结
简单来讲,一个数据版本,对于一个事务视图来说,除了下边讲到的“当前读”和自己的更新以外,有以下三种情况:

  • 版本未提交,不可见
  • 版本已提交,但是是在视图创建之后提交的,不可见
  • 版本已提交,并且是在视图创建之前提交的,可见

当前读

上边在将案例的时候提到的“读”都是开启事务后的普通的查询,在InnoDB中还存在当前读。
那什么是当前读?那什么时候才会是当前读?
只能读取当前记录的最新值称为“当前读”,更新数据、select …for update、select … lock 、lock in share mode都是当前读。
lock in share mode是加了读锁(共享锁,S锁),for update是写锁(排它锁,X锁)
举例(隔离级别为RR):
目前有一张表T,表中有一行记录id=1,c=1

事务A事务B事务C
begin;begin
select * from T where id=1;(结果为1,1)
update T set c=2 where id=1;
select * from T where id=1 for update;(查询结果为2)
select * from T where id=1 lock in share mode; (查询结果为2)
select * from T where id=1;(查询结果为1)select * from T where id=1; (查询结果为1)
update T set c=c+1 where id=1;
select * from T where id=1; (查询结果为3)
select * from T where id=1;(查询结果为1)
commit
commit

通过上图可以看到,事务C先执行的更新语句,更新完立刻就提交事务了,事务B使用for update查询是可以读到事务C提交的更新的,但不使用for update查询是读不到的,事务A同理;
当事务B将记录的c字段加1后,使用普通查询读到的结果直接就变成3了,说明update语句是先读最新值再更新。

当隔离级别为RC时,查询结果就不太相同了,因为RC是每次执行查询都会创建ReadView,因此读的时候都能读到已经提交的记录。


总结

  • 可重复读,查询能读到在事务启动(start transaction with consistent snapshot)前或者第一次执行查询语句(begin、start transaction)就已经提交完成数据
  • 读已提交,查询能读到每次查询语句执行前就已经提交完成的数据
  • 当前读,总是读取已经提交完成的最新数据

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

壹氿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值