MVCC


文章目录

  • MVCC
  • 概述
  • 版本链
  • 当前读和快照读
  • ReadView
  • 读已提交和可重复读的区别


概述

MVCC(Multi-Version Concurrency Control): 多版本并发控制

mysql通过其实现了读已提交、可重复读两种隔离级别,其也只在 可重复读 读已提交 两个级别下工作。

MVCC可以理解为行级锁的一个变种,很多情况下避免了加锁,性能更优( 是乐观锁的一种实现 )。

版本链

我们知道,mysql事务的隔离级别有四种,其中:

读未提交和串行化分别是最低和最高的隔离级别,读未提交是指可以查询到其他事务还没有提交的修改,始终都是查询最新的数据。串行化是指依次执行操作即对于并发操作进行加锁处理。

对于读已提交和可重复读,是 不可以查询到其他事务还没有提交的修改的,也就是说需要查询到修改之前的数据。

怎么能够得到修改之前的数据呢?我们知道,数据行的隐藏列有一个字段为 roll_pointer,其指向了这行数据的undo log,而对于del和update类型的log 有一个old roll_pointer又指向了这条undo log之前的undo log,它们一起构成了一条版本链。

当前读和快照读

  • 当前读
    像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
  • 快照读
    像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

ReadView

在读已提交和可重复读级别下查询数据,MVCC需要判断该条数据的版本链中的哪个版本是当前事务可见的。

事务在这两个隔离级别下执行读操作都属于快照读,快照读时会产生一个读视图(ReadView),记录并维护系统当前活跃事务(未提交事务)的ID

主要包含4个比较重要的内容:

  • m_ids :表示在生成 ReadView 时当前系统中活跃的读写事务的事务id列表。
  • min_trx_id :表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务id ,也就是 m_ids 中的最小值。
  • max_trx_id :表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。注意max_trx_id并不是m_ids中的最大值。
  • creator_trx_id :表示生成该 ReadView 的事务的 事务id 。

这样在快照读的时候只需要从最新的记录开始访问版本链,然后根据生成ReadView的去判断某个版本是否可见即可:

  1. 如果此版本的 事务id 等于 当前ReadView的creator_trx_id ,则表示此版本是该事务自己修改的,可以被访问,直接返回此版本即可
  2. 如果此版本的 事务id 小于 当前ReadView的creator_trx_id ,则表示此版本是该事务之前的事务提交的修改,可以被访问
  3. 如果此版本的 事务id 大于 当前ReadView的max_trx_id ,则表示在当前事务之后又开启了事务修改产生的版本,不可以被访问,继续沿着版本链向后访问
  4. 如果此版本的 事务id 在 当前ReadView的min_trx_id 和max_trx_id 之间,就需要判断 修改了此版本的事务id是否是活跃的,即是否在m_ids列表内,若在,则说明修改了此版本的事务此时还没提交呢,则此版本不能访问。若没有活跃,说明是已经提交的事务修改的,则可以被访问

若此记录的版本链访问完了,都不可访问,查询结果就没有这条记录。

读已提交和可重复读的区别

这两种不同的隔离级别,其实就是生成ReadView的时机不同

读已提交:顾名思义,已经提交的修改都可以获取到。也就是每次都生成新的ReadView。可以查询到最新的被提交的版本。

举个例子:

假设有一行数据 <id = 1, value = 1> 开启三个事务,A、B、C:

A:修改value 为 2

B:修改value 为 3

此时版本链为 3 -> 2 -> 1,3为最新的

C:查询id = 1,此时value = 1:生成一个ReadView,使用ReadView与当前C事务的id进行对比,因为A B都未提交所以从3 2 1一路遍历发现只有1可以访问,因此返回value = 1的版本

A:提交

C:查询id = 1,此时value = 2:生成一个新的ReadView,此时A提交了,因此与上面的查询的ReadView的活跃列表不同,A又是最早开启的事务,min_trx_id也不同,经过对比发现3不能访问,而2可以访问了,因此直接返回2

若B再提交,则C查询为 value = 3,过程与上面差不多。

可重复读:顾名思义,可以重复读取,即每次读取的值都一样,也就是首次获取某行数据时生成一个ReadView之后再查询则沿用此ReadView。因此只能查询到一开始查到的版本。

还是那行数据,A C两个事务(在可重复读级别下,不同的事务在未提交状态下不能对同一行数据进行修改,行级锁锁住了)

A:修改value 为 2

此时版本链为 2 -> 1,3为最新的

C:查询id = 1,此时value = 1,生成一个ReadView,此时活跃列表有[1(A),2(B)],当前id=3,max_trx_id =4,min_trx_id =1,一路对比发现只有1符合,因此返回1

A:修改为3

C:查询id = 1,此时value = 1,因为在可重复读级别下沿用了上面的那个ReadView.