MVCC是什么?有什么用?

前言:因为MVCC(Multiversion Concurrency Control,多版本并发控制)是一种事务隔离级别的无锁的实现方式,用于提高事务的并发性能,所以这里为不了解的MySQL并发的同学补充一下基础知识。

MySQL中因为并发会产生的问题有三种:脏读、不可重复读、幻读,MySQL为了解决这些并发问题采用了四种不同的事务隔离级别:读未提交、读已提交(行锁,读不会上锁)、可重复读(行锁,读和写都会上锁)、串行化(表锁),第二到第四种隔离界别分别解决了一种并发问题,例如读已提交解决了脏读问题。

同时,为了了解MVCC的具体实现,需要了解INNODB为每一行数据添加的三个隐藏列:DB_TRX_ID(当前数据被哪个事务所维护,这里就是这个事务的id,大小为6Byte)DB_ROLL_PTR(回滚指针:指向当前记录的上一个版本,存储于rollback_segment,大小为7Byte),ROW_ID(当你的表中没有指定主键id或者唯一的列时,会默认设置的一个id值,你看不见,主要用于生成MySQL的聚集索引的,在MVCC中没有太大的作用)。

​ 前言介绍了这么多,也没说为什么需要MVCC啊,难道用个行锁不行吗,也不会太影响并发的性能呀?

:当然不行,你的性能追求不高,但MySQL官方人员的性能追求高,如果你用了行锁,当一个事务锁住了某行数据的时候(假设是互斥锁),其他事务来读取这行数据的时候就会被阻塞,在一些高频访问的数据库中这会导致比较大的性能问题。为了提高数据库的性能,需要研发一种无锁、安全的并发方式,于是MVCC就诞生啦!

正文:

​ 既然MVCC是一种无锁的并发实现方式,那么就肯定需要维护多份不同版本的数据(有锁的话只需要锁住一条数据就行),所以MVCC使用了DB_TRX_IDDB_ROLL_PTR来维护多并发情况下不同版本数据之间的联系,通过DB_ROLL_PTR来指向上一条的旧版本数据,那么这些旧版本的数据放在那里呢?是放在 undolog 中的。当前表中的数据是最新的数据,通过DB_ROLL_PTR来指出这条数据的以前版本:

​ 那么,如果在隔离界别为:读已提交的情况下, 在某个正在执行的事务中,它该如何去 undolog 中找到一个最新的已提交的数据呢?这就需要我们下面即将讲到的ReadView了(这里先提前总结一下ReadView的本质:ReadView其实就是一堆事务id的集合,用来确保当前事务只能看见它该看见的数据,即确保不同事务之间的数据的隔离性)。

ReadView中主要包含4个比较重要的内容,分别是:

creator_trx_id :创建这个Read View 的事务ID,即创建者的事务ID。

说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都 默认为0

m_ids :表示创建ReadView时当前系统中未提交的事务的ID集合。

min_trx_id :表示创建ReadView时未提交的事务中最小的事务的ID。

max_trx_id:表示创建ReadView时系统中应该分配给下一个事务的id值,当前最大事务ID+1

最后形成的Read View的数据结构如下:

上面一通理论,其实白话就是:如果当前有两个事务正在执行,他们的事务id分别是:10 和 20,假设事务20创建了一个Read View,那么

  • creator_trx_id 就是 20
  • m_ids 就是 [10,20]
  • min_trx_id 就是 10
  • max_trx_id就是 20+1=21

​ 在读已提交的隔离级别下,每一次查询都会创建一个上述的ReadView,Read View决定当前事务能读到哪个版本的数据,从表记录到Undo Log历史数据的版本链,依次匹配,满足哪个版本的匹配规则,就能读到哪个版本的数据,一旦匹配成功就不再往下匹配。

遵循了以下可见性匹配规则:

规则说明:

  • trx_id = creator_trx_id:如果 trx_id 值等于创建Read View的事务Id,那么数据记录的最后一次操作的事务就是当前事务,该版本的记录对当前事务**可见。**如果不满足,继续下一条规则的判断。
  • trx_id < min_trx_id:如果 trx_id 值小于 Read View 中的 min_trx_id未提交事务中的最小的事务的id),表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。如果不满足,继续下一条规则的判断。
  • trx_id >= max_trx_id:如果trx_id 值小于 Read View 中的 min_trx_id ,表示这个版本的记录是在创建 Read View 后,其他事务启动并生成的,所以该版本的记录对当前事务不可见。如果满足该条件,直接通过DB_ROLL_PTR找到上一条旧版本数据 ,重头开始四条规则的判断;如果不满足,继续下一条规则的判断。
  • min_trx_id <= trx_id < max_trx_d:判断 trx_id 是不是在当前事务ID集合(m_ids)里面,如果在m_ids中,则代表Read View生成时刻,这个事务还未提交,在读已提交的隔离级别下,这条版本记录在前事务不可见如果不在m_ids中,则说明,这个事务在Read View生成之前就已经提交了,版本记录在前事务可见。(本质上第四步有两步判断)
  • 如果走完上述四个规则,结果都为不可见,那就需要沿着undolog中当前记录的DB_ROLL_PTR来寻找上一条旧版本记录,继续这四个判断。

一通理论,根本看不懂!!!那么,下面让我们直接看一个例子:

​ 上面有两个事务,id为10和20,其中事务10在事务20查询数据之前将balance从1000修改为了800,然后事务20执行查询数据,查出的结果是:balance=1000,即在读已提交的隔离级别下,事务20没有读取到事务10修改了但没有提交的数据

​ 那这个过程是怎么实现的?让我们根据前面提到的四条规则一一走一遍

1、trx_id = creator_trx_id,当前表中的最新数据里,DB_TRX_ID(trx_id)是10,而我们的creator_trx_id是20,所以不满足,继续下一条规则。

2、trx_id < min_trx_id,当前表中的最新数据里,DB_TRX_ID(trx_id)是10,而我们的min_trx_id是10(看上面图里的ReadView,我已经指出来了),说明这条最新数据不是在ReadView创建之前就存在的,不满足,继续下一条规则。

3、trx_id >= max_trx_id,当前表中的最新数据里,DB_TRX_ID(trx_id)是10,而我们的max_trx_id是21(看上面图里的ReadView,我已经指出来了),不满足,继续第四条规则的判断。(补充,如果当前trx_id是22,大于21,那么直接通过指针:0x123456去找到balance=1000的数据,从第一条规则开始判断!)

4、min_trx_id <= trx_id < max_trx_d,当前表中的最新数据里,DB_TRX_ID(trx_id)是10,而我们的min_trx_id是10,max_trx_id是21,满足条件!但是别急着高兴!!!我们还需要看一下m_ids中有没有DB_TRX_ID(trx_id):10,非常遗憾,!!!,说明当前表中的数据属于一个未提交的事务,我们不能读取!!!

5、既然上面4步都不能读取,那我们只能通过指针:0x123456去找到balance=1000的数据,从第一条规则开始判断。

6、trx_id = creator_trx_id,我们当前所处的数据是balance=1000的数据,而这条数据的DB_TRX_ID(trx_id)是1,而我们的creator_trx_id是20,所以不满足,继续下一条规则

7、trx_id < min_trx_id,我们当前所处的数据是balance=1000的数据,这条数据的DB_TRX_ID(trx_id)是1,而我们的min_trx_id是10!满足条件!我们能读取这条数据!!!我们读到了balance!它的值是1000!!!

至此,在读已提交的隔离级别下,我们通过了MVCC机制完成了一次数据读取!

上面讲了读已提交的隔离级别下的情况,那么其他隔离级别呢?

  • 读未提交:读未提交的隔离级别下,不需要锁也不需要mvcc,因为每一次修改都会直接修改数据源,会导致脏读问题的出现。
  • 读已提交:读已提交的隔离级别下,向上面说的,每次查询都会创建Read View来读取数据
  • 可重复读:可重复读的隔离级别下,我们要求同一个事务中的前后多次查询读到的数据一致,所以,我们就不能每次查询都创建Read View来读取数据,而是只在第一次查询的时候创建Read View,这样后续相同的查询就能拿到相同的数据了(MVCC在可重复读和读已提交这两个隔离级别中唯一区别就是:创建Read View的时机不同,其他机制都是一样的)
  • 串行化:直接利用表锁锁住数据,锁的粒度是最大的,并发性能也是最低的。

综上所述,MVCC只在读已提交可重复读这两个隔离级别中使用,目的是为了提高MySQL数据库的并发性能。

#面试题#
全部评论

相关推荐

点赞 12 评论
分享
牛客网
牛客企业服务