MySQL MVCC介绍

May 1, 2018


MVCC是什么?

MVCC的全称是Multi-Version Concurrency Control,通常用于数据库等场景中,实现多版本的并发控制

Multiversion concurrency control (MCC or MVCC), is a concurrency control method commonly used by database management systems to provide concurrent access to the database and in programming languages to implement transactional memory.

如果没有并发控制,那么如果同时有用户读写数据,那么可能出现读出的数据不一致的情况。比如说,进行银行账户A到B的转账,当A账户的钱被扣掉,而钱还没有加到B账户,此时用户查看自己的余额,会感觉钱凭空消失了。MySQL的隔离性就是用来解决这类问题的,而隔离性是通过不同的并发控制手段来实现的。对于刚才的问题,一种简单的并发控制方式,就是讲读写操作串行化,在账户间转账时,不允许查询账户,虽然这种方式可以解决问题,但无疑过于简单粗暴,效率极低。相比于串行化的并发控制,MVCC的优势在于读写影响,对于现代互联网读多写少的场景,这种方式性能明显更高。

MVCC是通过保存数据的多个版本来实现并发控制,当需要更新某条数据时,实现了MVCC的存储系统不会立即用新数据覆盖原始数据,而是创建该条记录的一个新的版本。对于多数数据库系统,存储会分为Data PartUndo LogData Part用来存储事务已提交的数据,而Undo Log用来存储旧版本的数据。多版本的存在允许了读和写的分离,读操作是需要读取某个版本之前的数据即可,和写操作不冲突,大大提高了性能。

MVCC的效果

假如MVCC是按照时间来判定数据的版本,在Time=1的时刻,数据库的状态如下:

Time Record A Record B
0 “Record A When time=0” “Record B when time=0”
1 “Record A When time=1”  

这个时候系统中实际存储了三条记录,Record A在时间0和1的各一条记录,Record B的一条记录,如果一个事务在Time=0的时刻开启,那么读到的数据是:

Record A Record B
“Record A When time=0” “Record B when time=0”

如果这个事务在Time=1的时候开启,那么读到的数据是:

Record A Record B
“Record A When time=1” “Record B when time=0”

上面的Case可以看到,对于读来讲,事务只能读到某一个版本及这个版本之前的最新一条数据,假如在Time=2的时候,事务Transaction X要插入Record C,并更新Record B,但事务还未提交,那么数据库的状态如下:

Time Record A Record B Record C
0 “Record A When time=0” “Record B when time=0”  
1 “Record A When time=1”    
2(Not Committed)   “Record B when time=2” “Record C When time=2”

这时候其它事务会读到的是什么了?在这个情况下,其它读事务所能看到系统的最新版本是系统处于Time=1的时候,所以依然不会读到Transaction X所改写的数据,此时读到的数据依然为:

Record A Record B
“Record A When time=1” “Record B when time=0”

基于这种版本机制,就不会出现另一个事务读取时,出现读到Record CRecord B还未被Transaction X更新的中间结果,因为其它事务所看到的系统依然处于Time=1的状态。

至于说,每个事务应该看到具体什么版本的数据,这个是由不同系统的MVCC实现来决定的,下文我会介绍MySQL的MVCC实现。除了读到的数据必须小于等于当前系统已提交的版本外,写事务在提交时必须大于当前的版本,而这里如果想想还会有一个问题,如果Time=2的时刻,开启了多个写或更新事务,当它们同时尝试提交时,必然会有一个事务发现数据库已经处于Time=2的状态了,那么这个事务该怎么办了?大家可以好好想想。

MySQL的MVCC

MySQL的Innodb引擎支持多种事务隔离级别,而其中的RR级别(Repeatable-Read)就是依靠MVCC来实现的,MySQL中MVCC的版本指的是事务ID(Transaction ID),首先来看一下MySQL Innodb中行记录的存储格式,除了最基本的行信息外,还会有一些额外的字段,这里主要介绍和MVCC有关的字段:DATA_TRX_IDDATA_ROLL_PTR,如下是一张表的初始信息:

Primary Key Time Name DATA_TRX_ID DATA_ROLL_PTR
1 2018-4-28 Huan 1 NULL

这里面为了便于说明,表中DATA_TRX_IDDATA_ROLL_PTR存的值是Mock的值:

  1. DATA_TRX_ID:最近更新这条记录的Transaction ID,数据库每开启一个事务,事务ID都会增加,每个事务拿到的ID都不一样
  2. DATA_ROLL_PTR:用来存储指向Undo Log中旧版本数据指针,支持了事务的回滚

最开始的记录无法回滚,所以DATA_ROLL_PTR为空。

这个时候开启事务A(事务ID:2),对记录进行了更新,但还没有提交,那么当前的数据为:

Transaction 1

可以看到,旧的数据会被存到Undo Log中,通过当前记录中的DATA_ROLL_PTR关联,那么如果另一个事务中想读取该数据,读到的会是什么数据了?假如说另一个事务B在事务A之后开启(事务ID:3),既然我们最开始说Innodb的MVCC是基于事务ID做的,那么既然事务B的事务ID比事务A的大,那么事务B就可以独到A还未提交的数据了,这明显和Innodb RR的定义不符合。实际上,事务读取时,判断应该读取哪个版本的记录,有一个较为复杂的逻辑,不是单纯的和记录上的事务ID进行比较,假设当前读的事务ID为read_id,记录当前存储的事务ID为tid,当前系统中未提交的事务中(Read_View中)的最大最小事务ID分别为max_tidmin_tid,那么数据可见性判断流程为:

Transaction visibility

通过上图(这个图是通过分析网上的一些博客内容得到的,和实际MySQL的逻辑细节可能不一致),在来分析上文提到的Case,由于事务B的事务ID不满足read_id=tid||tid<min_tid的条件,且该记录当前有DATA_ROLL_PTR,所以最后该事务B实际读取的是Undo Log中的记录:

Primary Key Time Name DATA_TRX_ID DATA_ROLL_PTR
1 2018-4-28 Huan 1 NULL

需要注意的是,MySQL的MVCC和理论上的MVCC实际有所差异,MySQL同一时刻只允许一个事务去操作某条数据,该条数据上的操作实际是串行的,也就是说一条记录的有用版本实际就只会有当前记录和一条Undo Log记录,是悲观锁的操作方式,而MVCC的定义上实际是乐观锁的操作方式,某一时刻记录可以存在很多个版本。


上一篇博客:MySQL 5.7 insert on duplicate key问题
下一篇博客:Git笔记