最近在学习MVCC,在学习过程中,很疑惑RC(读已提交)和RR(可重复度)级别都用到了MVCC来进行不加锁的读,但是为什么RR级别可以解决幻读,对于RC级别不行?

         本文主要解答上面那个疑问,关于MVCC的,还请移步两位大神的博客,进行深入学习:MySQL 8.0 MVCC 源码解析, MySQL事务与MVCC如何实现的隔离级别?

        MVCC即多版本并发控制,能够保证多个读请求之间不会进行阻塞,根据事物隔离级别和事物id来确定当前事物能够查询到的数据的版本。对于每行记录来说,可能会存在多个版本,而这些版本会使用使用链表进行关联起来,从而控制一个事务能够查询到的数据的版本。

       首先需要明白事务的隔离级别和MVCC有密切的关系,你能深入事务的隔离级别,那么对于理解MVCC是很有帮助的,反过来,你能理解MVCC,再去理解事务的隔离级别也会有不一样的感受。

 

面试题:MySQL 的可重复读怎么实现的?

InnoDB 会在每行记录后面增加三个隐藏字段:

DB_ROW_ID:行ID,随着插入新行而单调递增,如果有主键,则不会包含该列。

DB_TRX_ID:记录插入或更新该行的事务的事务ID。

DB_ROLL_PTR:回滚指针,指向 undo log 记录。每次对某条记录进行改动时,该列会存一个指针,可以通过这个指针找到该记录修改前的信息 。当某条记录被多次修改时,该行记录会存在多个版本,通过DB_ROLL_PTR 链接形成一个类似版本链的概念。

          上面的是每行记录都会有隐藏列,同一记录的修改记录会被链接成一个链表,而对于每个事物来说,访问记录时,是从每行记录的头部开始算起的。对于 RC(READ COMMITTED) 和 RR(REPEATABLE READ) 隔离级别的实现就是通过上面的版本控制来完成。

 

思考下面一个问题:

在RC(读已提交)和RR(可重复度)级别下,MVCC都会生效,那么为什么RC不可以解决幻读,而RR可以解决幻读?

         原因: 两种隔离界别下的核心处理逻辑就是判断所有版本中哪个版本是当前事务可见的处理针对这个问题InnoDB在设计上增加了ReadView的设计,ReadView中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids。

         以上内容是对于 RR 级别来说,而对于 RC 级别,其实整个过程几乎一样,唯一不同的是生成 ReadView 的时机,RR 级别只在事务开始时生成一次,之后一直使用该 ReadView。而 RC 级别则在每次 select 时,都会生成一个 ReadView。

 

简易版的说:
       当前活跃的事务列表集合,从小到大的顺序排列,在这个集合中的事务列表说明并未提交,对于本事务来说是不可见的,比该事件列表最大的id还要大,说明本事务执行时,修改记录的事务还未开始,所以比该事件列表最大的id还要大的,那么就是不可见的

在RR级别下,事务执行第一个 select 语句的时候,会生成一个当前时间点的事务快照 ReadView,主要包含以下几个属性:
trx_ids:生成 ReadView 时当前系统中活跃的事务 Id 列表,活跃的事务 Id 列表从小到大的顺序排列,就是还未执行事务提交的。
creator_trx_id:生成该 ReadView 的事务的事务 Id。


在访问某条记录时,会根据该事务中的事务快照 ReadView进行判断:

  1. 行记录中的事务id是否为该事务本身的id,相同则说明自己创建的,可见
  2. 行记录中的事务id比事务快照 ReadView中最小的还小,表明该记录为之前提交的,能够被当前事务访问。
  3. 行记录中的事务id比事务快照 ReadView中最大的事务id还要大,表明该记录为之后提交的,不能被当前事务访问
  4. 行记录中的事务id在活跃的事务 Id 列表之间,那么就使用二分,判断是否能在活跃集合中找到该事务id,能找到,说明该事务快照时,还未提交,本事务无法访问,如不在活跃事务集合中,说明已提交,可以访问

      在进行判断时,首先会拿记录的最新版本来比较,如果该版本无法被当前事务看到,则通过记录的 DB_ROLL_PTR 找到上一个版本,重新进行比较,直到找到一个能被当前事务看到的版本。

       下面的面试可以先不说的,先说上面的,如果追问再详细说。
接下来进入正题,以 RR 级别为例:每开启一个事务时,系统会给该事务会分配一个事务 Id,在该事务执行第一个 select 语句的时候,会生成一个当前时间点的事务快照 ReadView,主要包含以下几个属性:

trx_ids:生成 ReadView 时当前系统中活跃的事务 Id 列表,就是还未执行事务提交的。

up_limit_id: 取 trx_ids 中最小的那个,trx_id 小于该值都能看到。

low_limit_id:生成 ReadView 时系统将要分配给下一个事务的id值,trx_id 大于等于该值都不能看到。

creator_trx_id:生成该 ReadView 的事务的事务 Id。

有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

  1. 如果被访问版本的trx_id与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  2. 如果被访问版本的trx_id小于ReadView中的up_limit_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
  3. 如果被访问版本的trx_id大于等于ReadView中的low_limit_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
  4. 如果被访问版本的trx_id属性值在ReadView的up_limit_id和low_limit_id之间,那就需要判断一下trx_id属性值是不是在trx_ids列表中。如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

在进行判断时,首先会拿记录的最新版本来比较,如果该版本无法被当前事务看到,则通过记录的 DB_ROLL_PTR 找到上一个版本,重新进行比较,直到找到一个能被当前事务看到的版本。
      
      以上内容是对于 RR 级别来说,而对于 RC 级别,其实整个过程几乎一样,唯一不同的是生成 ReadView 的时机,RR 级别只在事务开始时生成一次,之后一直使用该 ReadView。而 RC 级别则在每次 select 时,都会生成一个 ReadView。

      此时我们再思考一下RC和RR的区别:RC  读已提交、     RR  可重复读
      在RC模式下会存在同一个SQL查询出不同的结果,相对于数据的修改,为什么会这样,就因为在RC模式下每次都会生成一个ReadView, 

        使用MVCC来解释读已提交造成的不可重复读问题, 每次select都会生成一个ReadView, 以发生时间4为例,如下图

 

RC的情况

查看id = 1的数据,根据上面ReadView的规则判断可见,

trx_ids  为[1],此时只有事务1正在处于活跃状态
up_limit_id  = 1,  此时最小的事务编号为1
low_limit_id = 3, 下次要分配的事务id
creator_trx_id = 1,当前事务

        所以可以得知,  当前数据的事务版本号在up_limit_id 、low_limit_id之间, 符合条件四,然后对trx_ids  进行二分查询后发现查找不到,此时就说明事务id = 2的已被提交, 当前数据可以被查看到。所以发生时间4时,可以看到名称已经被修改成了 李四,也就造成不可重复读的现象。
        
RR的情况
查看id = 1的数据,根据上面ReadView的规则判断可见,  

 trx_ids  为[1],此时只有事务1正在处于活跃状态
up_limit_id  = 1,  此时最小的事务编号为1
low_limit_id = 2, 下次要分配的事务id
creator_trx_id = 1,当前事务

          所以可以得知,  当前数据的事务版本号等于low_limit_id最大的版本号, 符合条件3,所以当前行不能被访问到,该版本无法被当前事务看到,则通过记录的 DB_ROLL_PTR 找到上一个版本,通过undo log日志查找到上一个版本进行对比后,发现数据事务id和当前事务id相同,所以可以访问到名称为张三的数据。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐