为什么需要MVCC?
什么情况下才会用到MVCC?
MVCC是如何实现的?

背景

MVCC,英文全称是 Multiversion Concurrency Control,翻译过来就是多版本并发控制。

提到并发,那么出发点基本上都是效率问题,MVCC也不例外,其目的就是为了提高读写效率。
我们知道,数据库通常使用锁来实现隔离性,也就是说锁住资源之后会禁止其他线程访问该资源,但是一般情况下都是读多写少的场景,这也就催生了读写锁,因为往往读与读之间没有必要互斥,但是读写锁的效率依然不能满足大家日益增长的效率需求,能不能让读和写也不冲突呢?能!MVCC做到了

机制

MVCC将数据添加了版本的概念,每一个事务,将只会看到符合自己“权限”的特定版本的数据,通过这种方式,读与写也可以做到不冲突了。
这个所谓的“权限”,就是判断事务可见哪个数据版本的判断逻辑,这个逻辑在不同的隔离级别下也会有差异。
比如写入事务内的读取操作将能看到最新写入的版本的数据,但是其他事务只能看到之前版本的“快照”数据。

我们常听到的“快照读”和“当前读”,就是指读取快照数据 和 读取最新已提交数据。

不仅是MySQL,包括OraclePostgreSQL等其他数据库系统也都实现了MVCC,但各自的实现机制不尽相同,因为MVCC没有一个统一的实现标准,典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。
我们这里讨论的是MySQLInnoDBMVCC

实现原理

如何存储

InnoDB中的每个数据行,其实不只是我们自己定义的字段,还有一些隐藏的字段,而用以实现MVCC的就是三个隐藏字段:
DB_TRX_ID:6 byte,是最近修改(修改/插入)事务 ID,记录创建这条记录/最后一次修改该记录的事务 ID。也就是上面所谓的“版本号”。
DB_ROLL_PTR:7 byte,回滚指针,指向这条记录的上一个版本,即undolog中的回滚记录。
DB_ROW_ID:6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以此ID产生一个聚簇索引。

  • 在Insert操作时,只会记入当前事务ID,因为是新插入,所以不会有回滚地址。
  • 在update操作时,旧数据行不变,会复制一个新的行,记入当前事务ID,并将回滚地址中填入本数据行上一个版本的undolog日志的地址,便于寻找上一个版本的数据。
  • 在delete操作时,与update操作类似,此时不会真正删除数据,只是用另一个隐藏字段记录删除状态。
  • select操作只是读取相应的数据,不会产生数据副本。

而之前所说的“版本”的概念,就是用事务ID来标识的,用事务ID当做版本号,来决定哪些事务可以看到哪个版本的数据。而这每个版本的数据就是复用undolog来存储的。每个事务查看数据的可见性的过程,也就是选择可见版本的过程。

如何工作

说到MVCC具体如何工作,还需要引入一个概念,Read View 读视图。
ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。其中最主要的与可见性相关的属性如下:


up_limit_id:The read should see all trx ids which are strictly smaller (<) than this value. In other words, this is the low water mark".
当前已经提交的事务号 + 1,事务号 < up_limit_id ,对于当前Read View都是可见的。理解起来就是创建Read View视图的时候,之前已经提交的事务对于该事务肯定是可见的。

low_limit_id:The read should not see any transaction with trx id >= this value.
In other words, this is the "high water mark".
当前最大的事务号 + 1,事务号 >= low_limit_id,对于当前Read View都是不可见的。理解起来就是在创建Read View视图之后创建的事务对于该事务肯定是不可见的。

trx_ids:Set of RW transactions that was active when this snapshot was taken.
活跃事务id列表,即Read View初始化时当前未提交的事务列表。所以当进行RR读的时候,trx_ids中的事务对于本事务是不可见的(除了自身事务,自身事务对于表的修改对于自己当然是可见的)。理解起来就是创建RV时,将当前活跃事务ID记录下来,后续即使他们提交对于本事务也是不可见的。

举个栗子,说明下工作过程:
假设,此时,1、2事务已经提交,3事务尚未提交
此时如果有一个事务4来访问此字段,那么此时,事务4的Read View中,事务3还未提交,在trx_ids中,属于不可见事务,而事务1、2均已提交,小于up_limit_id,则属于可见范围,所以能够看到value是222

再假如说,此时事务3提交了,又有一个事务5来更新此字段value为555,那么此时事务4的第二次查询行为将会看到什么呢?这里就会体现出不同隔离级别下的不同行为:

  • 假如隔离级别为可重复读,那么Read View只会在第一次查询时生成,且在整个事务中只会生成这一次,所以依然只能读取到value为222
  • 假如隔离解蔽为已提交读,那么Read View会在再次查询时重新生成,所以此时将会能够看到value已经变成了333

而根据上面的描述,我们能够明白MVCC的另外一个特性:
MVCC只会在已提交读可重复读下生效

  • 对于未提交读的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本即可
  • 对于可串行化的事务来说,select语句也需要使用加锁的方式来访问记录,所以根本不需要`MVCC

意义

1. 读写之间阻塞的问题

通过MVCC可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。

提高并发的演进思路:

  • 普通锁,只能串行执行;
  • 读写锁,可以实现读读并发;
  • 数据多版本并发控制,可以实现读写并发。

2. 降低了死锁的概率

因为InnoDBMVCC采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行

3. 解决一致性读的问题

一致性读也被称为“快照读”,当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。

引申讨论 - InnoDB的RR级别是否解决了幻读?

InnoDB的RR级别下的情况是,在“快照读”下,因为MVCC和间隙锁的存在,确实不再会出现幻读的情况,但是如果是“当前读”下,幻读情况将会是依然存在的。
这个问题确实很有意思,相比之下,结果并不是那么重要,大家了解到原理就好~

相关文章
https://github.com/Yhzhtk/note/issues/42
http://mysql.taobao.org/monthly/2017/06/07/
https://tech.meituan.com/2014/08/20/innodb-lock.html

引用

  • https://github.com/zhangyachen/zhangyachen.github.io/issues/68
  • https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc
  • https://zhuanlan.zhihu.com/p/66791480
  • https://blog.csdn.net/qq_35190492/article/details/109044141
  • https://blog.csdn.net/SnailMann/article/details/94724197
  • https://segmentfault.com/a/1190000037557620
  • https://zhuanlan.zhihu.com/p/377162929
  • https://github.com/hanchuanchuan/bingo2sql
  • https://zhuanlan.zhihu.com/p/35574452
  • https://www.modb.pro/db/75331
  • https://juejin.cn/post/7001357238648438821