深入分析MVCC机制

1.什么是MVCC机制

MVCC机制是多版本并发控制机制。

MySQL可以在可重复读隔离级别下保证事务较高的隔离性,同样的SQL查询语句在一个事务里多次执行查询结果相同,就算其它事务对数据进行修改也不会影响当前事务SQL语句的查询结果。

这个隔离性就是依靠MVCC(Multi-Version Concurrency Control)机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。

MySQL在读已提交和可重复读的隔离界别下都实现了MVCC机制。

下面举个例子说明一个MVCC机制:

MVCC机制名称是多版本并发机制,主要是为了实现读写操作时的无锁并发的,对于串行化隔离级别来说,为了保证读写数据的操作不冲突,对每行进行操作的时候都需要加锁,比如读数据的时候,会对数据行加读锁,当写数据的时候,会对数据行加写锁,即串行化的隔离效果是使用加锁互斥来实现的,进行任意操作的时候都会对数据行加锁,非常影响性能。

但是在读已提交和可重复读的隔离级别下面,使用了MVCC机制保证了读写并发,读数据时不会被写锁卡住,读数据时也不会卡住写操作,这就是MVCC机制,使用这种机制可以保证隔离性不是使用加锁互斥来实现的,提升了数据库的读写并发性能。

MVVC机制最大的特点就是:不适用锁互斥即可实现隔离性和读写并发。

2.undo日志版本链和read view机制详解

2.1undo日志版本链

undo日志版本链是指一行数据被多个事务依次修改后,在每个事务修改完之后,MySQL会保留修改前的数据undo回滚日志,并且使用两个隐藏字段trx_id(操作事务id)和roll_pointer(回滚指针,回滚的时候方便知道要回滚到什么状态)把这些undo日志串联起来形成一个历史记录版本链,如下图所示:

2.2从RR和RC隔离级别窥探机理

我们使用Excel表格模拟一下RR和RC事务的隔离性,分析在多个事务操作更新的情况,RR事务隔离级别的事务读取数据和RC事务隔离级别事务读取数据的整体情况。

下面的是整体的流转流程:

启动了三个RR隔离级别的事务负责更新数据,更新的数据表是account,更新数据行是id=1的数据行,更新balance数据格的数据,这三个事务的trx_id(事务id)分别为100,200和300。

启动两个RR隔离级别的事务负责读取数据,读取的数据表是account,读取的数据行是id=1的数据行。

启动一个RC隔离级别的事务负责读取数据,读取的数据表是account,读取的数据行是id=1的数据行。

1.首先所有事务都启动了,事务100和事务200先进行一个预热,均执行了两次更改其他表的操作,初始化时account表中id=1的数据行,balance数据为0。

2.事务300 对account表中id=1的数据行中的balance数据进行+500,并随之进行提交,此时balance的数据为500。

3.查询事务1 查询了一次account表中id=1的数据行,查询出balance数据为500;查询事务3查询了一次account表中id=2的数据行,查询出balance数据为500。

4.事务100 对account表中id=1的数据行中的balance数据进行+300,并未提交,又对account表中id=1的数据行中的balance数据进行+200,并随之提交了,此时balance的数据为1000。

5.查询事务1 查询了一次account表中id=1的数据行,查询出balance数据为500。

6.查询事务2 查询了一次account表中id=1的数据行,查询出balance数据为1000。

7.查询事务3 查询了一次account表中id=1的数据行,查询出balance数据为1000。

8.事务200 对account表中id=1的数据行中的balance数据进行+500,并未提交,又对account表中id=1的数据行中的balance数据进行+300,并随之提交了,此时balance的数据为1800。

9.查询事务1 查询了一次account表中id=1的数据行,查询出balance数据为500。

10.查询事务2 查询了一次account表中id=1的数据行,查询出balance数据为1000。

11.查询事务3 查询了一次account表中id=1的数据行,查询出balance数据为1800。

可以看到其中完全符合之前我们分析的RR隔离级别和RC隔离级别读取数据时的表现,接下来我们详细分析一下其机理是如何实现的,为什么做到这样的时可以实现读写并发操作呢?

为了回答这个问题,我们需要详细介绍一下MVCC机制。

2.3深入理解MVCC和read view

2.3.1初识read view

我们将刚刚使用Excel分析过的执行流程生成的undo版本链拉过来进行分析,下面时undo版本链:

简单先过一下undo log链的整体构造流程。

刚开始使用INSERT语句,向数据库中进行添加数据。

此时生成的第一条数据是事务80进行添加的,roll_pointer隐藏行的指针会指向INSERT的undo log。

紧接着事务300对balance数据进行了修改,则又会生成一条trx_id为300的undo日志,其roll_pointer指针会指向上一条日志数据,且事务300进行了提交,所以此时commit提交标记会被标记在该undo log处。

事务100又对数据进行了一次修改,对balance进行了一次+300的操作,将该数据行的balance数据修改为了800,则会生成一条trx_id为100的undo log,并且roll_pointer指针会指向上一条日志数据,但是事务并没有进行提交,所以commit提交标记并没有修改。

事务100又对数据进行了一次修改,对balance进行了一次+200的操作,将该数据行的balance数据修改为了1000,则会生成一条trx_id为100的undo log,并且roll_pointer指针会指向上一条日志数据,此时事务100进行了一次提交,所以commit提交标记会被修改到这条undo log日志上。

事务200对数据进行了两次修改,最终提交了(省略了)

当整个undo log链构建完成后,就要开始讨论其它事务在构建过程中读取时的机理运转流程了,即MVCC机制是如何运转的。

在事务启动之后,在MVCC机制中会为每个启动的事务生成三个数组(类比未数组,并不是真正的数组),对事务进行分类:

在事务正式启动的一瞬间(即执行了一条CRUD的SQL指令),MVCC机制会开始收集此时所有未提交的事务,并获取到所有未提交的事务中trx_id的最小值(min_id)和最大值(max_id),小于最小值的都是已经提交的事务,大于最大值都是未开始的事务。

在形式上其实就是生成了三个数组,一个以提交事务的数组,一个未提交和已提交事务的数组(未提交的事务可能会随着时间的推移成为已提交的事务),一个未开始的事务的数组。

在RR和RC事务隔离级别,当事务开启时会根据所有提交事务id的数组(数组中最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何SQL查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。

需要注意的是,RC事务隔离级别相比于RR事务隔离级别具有一些不同之处,RR事务隔离级别在事务真正启动之后,生成的read view就不会变化了,但是RC事务隔离级别启动之后,每次RC事务再进行执行查询语句的时候,都会重新生成一次read view。

2.3.2版本链对比规则

1.如果row的trx_id落在绿色部分(trx_id < min_id),表示这个版本是已提交事务生成的,则数据是可见的。

2.如果row的tex_id落在红色部分(trx_id > max_id),表示这个版本是由将来启动的事务生成的,是不可见的。

3.如果row的trx_id落在黄色部分(min_id <= trx_id <= max_id),那就包含两种情况:

    • row的trx_id在视图数组 中,表示这个版本是由还没提交的事务生成的,不可见(若row的trx_id就是当前自己的事务是可见的)
    • row的trx_id不在视图数组 中,表示这个版本是已经提交了的事务生成的,可见

2.3.3根据版本链分析事务中执行结果的原因

下面是使用Excel表格模拟RR事务隔离级别和RC事务隔离级别的整体执行流程(新增了readview):

我们通过undo log版本链和Excel执行流程,使用readview来梳理一下事务中执行结果的原因。

我们按以下的步骤帮助大家理解梳理全流程:1.readview的构建。2.readview在版本链中的对比使用。

2.3.3.1readview构建

先来看SELECT 1的readview事务构建:

再来看SELECT 2的readview事务构建:

最后看RC事务隔离级别SELECT 3的review事务构建:

readview的构建已经讲清楚了,现在来看事务是如何使用readview去配合undo log实现隔离级别读取数据的。

2.3.3.2SLECET 1事务读取数据

从Excel来看第一步的操作:

事务300执行了UPDATE操作后,生成的undo log版本链如下:

当SELECT 1事务去查询的时候,会生成一个readview,即[100, 200] 300,事务会拿着readview去undo版本链中进行对比查询数据,从最新的undo log开始对比查询,发现300 > min_id(100),300 <= max_id(300),且300不在未提交事务,则本行undo log的数据是可见的,则直接返回对应数据,结束。

接下来去看Excel中的第二步操作:

事务100执行了UPDATE操作后,生成的undo log版本呢链如下:

当SELECT 1再去查询数据的时候,其readview不会变化,仍然是一开始生成的[100, 200] 300,查询数据时会从undo log版本链最后一条数据开始对比,最后一条数据的trx_id的值是100,100 >= min_id(100),100 <= max_id(300),且100在数组中,则改行undo log数据是不可见的。紧接着通过roll_pointer向上找undo log数据,再通过readview去判断,trx_id是300,300 >= min_id(100),300 <= max(300),且300不在数组中,则改行undo log数据是可见的。

最后看Excel中的最后一步操作:

事务200执行了UPDATE操作之后,生成的undo log版本链如下:

当SELECT 1去查询数据的时候,其readview不会变化,后四个undo log数据,其trx_id均 <= min_id(100),>= max_id(300),且trx_id都在数组中,该四行的undo log数据是不可以被看见的,但是trx_id为300的数据是可见的,因为300不在数组里,所以最终返回的数据是500。

3.总结

RR和RC事务隔离级别,借助readview版本链,实现这种隔离级别,全得益于MVCC机制中undo log版本链和readview读视图来实现的,无需像串行化隔离级别一样,使用锁机制才能实现事务隔离级别。