1、简介
MVCC(Multi-Version Concurrency Control)多版本并发控制,是用来在数据库中控制并发的方法,实现对数据库的并发访问用的。在MySQL中,MVCC只在读取已提交(Read Committed)和可重复读(Repeatable Read)两个事务级别下有效。其是通过Undo日志中的版本链和ReadView一致性视图来实现的。MVCC就是在多个事务同时存在时,SELECT语句找寻到具体是版本链上的哪个版本,然后在找到的版本上返回其中所记录的数据的过程。
首先需要知道的是,在MySQL中,会默认为我们的表后面添加三个隐藏字段:
(1)DB_ROW_ID:行ID,MySQL的B+树索引特性要求每个表必须要有一个主键。如果没有设置的话,会自动寻找第一个不包含NULL的唯一索引列作为主键。如果还是找不到,就会在这个DB_ROW_ID上自动生成一个唯一值,以此来当作主键(该列和MVCC的关系不大);
(2)DB_TRX_ID:事务ID,记录的是当前事务在做INSERT或UPDATE语句操作时的事务ID(DELETE语句被当做是UPDATE语句的特殊情况,后面会进行说明);
(3)DB_ROLL_PTR:回滚指针,通过它可以将不同的版本串联起来,形成版本链。相当于链表的next指针。
2、ReadView
ReadView一致性视图主要是由两部分组成:所有未提交事务的ID数组和已经创建的最大事务ID组成(实际上ReadView还有其他的字段,但不影响这里对MVCC的讲解)。比如:[100,200],300。事务100和200是当前未提交的事务,而事务300是当前创建的最大事务(已经提交了)。当执行SELECT语句的时候会创建ReadView,但是在读取已提交和可重复读两个事务级别下,生成ReadView的策略是不一样的:读取已提交级别是每执行一次SELECT语句就会重新生成一份ReadView,而可重复读级别是只会在第一次SELECT语句执行的时候会生成一份,后续的SELECT语句会沿用之前生成的ReadView(即使后面有更新语句的话,也会继续沿用)
3、版本链
所有版本的数据都只会存一份,然后通过回滚指针连接起来,之后就是通过一定的规则找到具体是哪个版本上的数据就行了。假设现在有一张account表,其中有id和name两个字段,那么版本链的示意图如下:
而具体版本链的比对规则如下,首先从版本链中拿出最上面第一个版本的事务ID开始逐个往下进行比对:
(其中min_id指向ReadView中未提交事务数组中的最小事务ID,而max_id指向ReadView中的已经创建的最大事务ID)
(1)如果落在绿色区间(DB_TRX_ID < min_id):这个版本比min_id还小(事务ID是从小往大顺序生成的),说明这个版本在SELECT之前就已经提交了,所以这个数据是可见的。或者(这里是短路或,前面条件不满足才会判断后面这个条件)这个版本的事务本身就是当前SELECT语句所在事务的话,也是一样可见的;
(2)如果落在红色区间(DB_TRX_ID > max_id):表示这个版本是由将来启动的事务来生成的,当前还未开始,那么是不可见的;
(3)如果落在黄色区间(min_id <= DB_TRX_ID <= max_id):这个时候就需要再判断两种情况:
- 如果这个版本的事务ID在ReadView的未提交事务数组中,表示这个版本是由还未提交的事务生成的,那么就是不可见的;
- 如果这个版本的事务ID不在ReadView的未提交事务数组中,表示这个版本是已经提交了的事务生成的,那么是可见的。
(4)如果在上述的判断中发现当前版本是不可见的,那么就继续从版本链中通过回滚指针拿取上一个版本来进行上述的判断。
例子:
开3个事务
(1)事务300先执行,修改了id=1中的name字段为monkey300,然后提交了事务
(2)事务100跟着执行,修改了id=1中的name字段为monkey100,然后查询id=1的name字段的值,查询完后提交事务
(3)事务200最后执行,修改了id=1中的name字段为monkey100,然后查询id=1的name字段的值,查询完后提交事务
分析:
(1)此时select生成的事务一致性视图ReadView中存在一个正在执行的事务数组[100,200]和最大的事务id300,最小的活跃事务id是100
(2)事务100查询id=1数据的值,先和最大的事务id300比较,发现小于当前最大事务id,并且事务100发现自己存在正在执行的事务数组[100,200]中,所以当前事务100修改id=1的数据就是不可见的,所以查询id=1的数据是根据回滚指针找上一个可见的事务数据,也就是事务300提交的数据
(3)事务300和事务100已经完成,此时正在执行的事务数组中还剩事务200,根据(2)一样的推算,当前事务200修改id=1的数据是其他事务查询不到的,也就是不可见的
(4)如果事务100修改后没有提交事务,事务200通过推算查到事务100,然后用事务100比较是和步骤(2)一样的结果,最后通过回滚指针回滚到的版本都是事务300提交的数据是可见的
ReadView读视图中包含了四个核心字段,也是读取数据的判断依据:
字段 | 含义 |
m_ids | 当前活跃的事务ID集合 |
min_trx_id | 最小活跃事务ID |
max_trx_id | 当前最大事务ID(已创建的最大事务id) |
ReadView一共有四种匹配规则:
条件 | 能否访问 | 说明 |
trx_id < min_trx_id | 可以访问该版本 | 成立,说明数据已经提交了。 |
trx_id > max_trx_id | 不可以访问该版本 | 成立,说明该事务是在ReadView生成后才开启的。 |
min_trx_id <= trx_id <= max_trx_id | 如果trx_id不在m_ids中,那么可以访问该版本 | 成立,说明数据已经提交。 |