图解MVCC多版本并发控制

2,221 阅读9分钟

前言

现在已经知道了sql四种隔离级别分别为 RU、RC、RR和串行化。

而我们熟悉的MySQL的默认隔离级别是第三种 RR(可重复读)。相对于SQL标准的RR,MySQL的RR是基于MVCC机制实现的,在此隔离级别下,是可以防止脏写、脏读、不可重复读和幻读。

MVCC是什么样的一种机制呢?

其实MVCC是 multi-version concurrent control(多版本并发控制)的缩写,是基于undo log 多版本链条 + ReadView机制来实现。

下面说说MVCC机制的实现和基于MVCC实现的RC和RR。

1. 理解mvcc前奏,undo log 版本链是什么?

关于undo log我们之前也讲过。每条数据都有两个隐藏字段,分别是trx_id和roll_pointer。trx_id就是最近更新这条数据的事务id,roll_pointer就是指向更新这个事务之前生成的undo log。

假设事务A(id=50)插入了一条数据,插入的值为A,那么,对应的trx_id为50,roll_pointer指向一个空的undo log,因为之前是没有这条数据。

图 1-1

接着事务B过来更新了一下这条数据,把值改成了B值,事务B的id为60,rooll_pointer指向这个实际的undo log回滚日志:

图 1-2

这条undo log就记录了事务B的事务id,修改后的值B以及roll_pointer指向的是它修改前的undo log

接着事务C又来修改一下这个值为值C,所对应的事务id为70,如图所示:

图 1-3

这里可以看到roll_pointer指向了本次修改之前的undo log,并且和事务A的undo log也串联了起来。

所以这里可以清楚的了解到,每次修改数据都会更新trx_id和roll_pointer这两个字段,同时之前的多个数据快照对应的undo log,会通过roll_pointer指针串联起来,形成的一个版本链。

2. 基于 undo log 多版本链条实现的ReadView是什么样的?

当执行一个事务时,就生成一个ReadView,里面包含几个部分:

  • m_ids: 此时有哪些事务在MySQL中执行还没提交的
  • min_trx_id:m_ids里面最小的值
  • max_trx_id: MySQL下一个要生成的事务id,就是最大事务id
  • creator_trx_id:当前事务的id

下面通过一个例子理解ReadView的用处。

假设数据库里已经存在了一条数据,由之前的事务插入的,其事务id为32,初始值为原始值。

图 2-1

接下来,有两个事务 A 和 B 并发过来执行。事务A的事务id为45,事务B的事务id为59。事务B要更新这行数据,事务A要查询这行数据。

图 2-2

现在事务A开启了一个ReadView,在这个ReadView里面,m_ids包含了事务A和事务B的两个id,45和59,然后min_trx_id是45,max_trx_id是60,creator_trx_id是当前开启事务的id 45,就是事务A。

图 2-3

现在事务A进行第一次查询,首先会走一个判断,判断一下这行数据的trx_id是否小于ReadView的min_trx_id,此时,发现trx_id是32,是小于ReadView里的min_trx_id(45),说明事务A开始之前,修改这行数据的事务早就提交了,所以是可以查到这行数据的。

图 2-4

接着事务B过来修改这行数据,把原始值改成了值B,然后这行数据的trx_id就变成了59,同时roll_pointer指向修改之前生成的undo log 。

图 2-5

这个时候事务A再次查询,会发现这行数据的trx_id是59,是大于ReadView里的min_trx_id(45),同时小于max_trx_id(60)的。说明更新这条数据的事务有可能存在ReadView的m_ids中,然后判断m_ids里面是否存在trx_id=59的事务,刚好m_ids里面是存在59这个事务id,证实是跟自己同一时段并发执行的事务,该数据不能查询。

图 2-6

既然这行数据不能查询,应该返回什么数据?

这时就会顺着这条数据的roll_pointer的undo log日志链条往下找,就会找到最近的一条trx_id=32的undo log。说明这个undo log版本必然是在事务A开启之前就执行提交的。

看到这里,就体会到了undo log版本链条的作用了,通过保存快照链条,让你快速读到之前的快照值。

通过以上 undo log版本链条 + ReadView 就可以保证了事务A不会读到并发执行的事务B更新的值。

接着看,假设事务A自己更新了这行数据,改成值A,trx_id更新为45,同时保存之前事务B修改的值得快照

图 2-7

此时事务A再来查询这行数据,发现trx_id=45,与ReadView里的creator_trx_id(45)是一致的,说明这是自己修改的这行数据,当然可以被查询到。

图 2-8

接着在事务A执行事务期间,突然开启了一个事务C,事务id为78,然后更新了这行数据为值C,并提交了。

图 2-9

这个时候事务A再去查询这行数,发现trx_id=78,大于ReadView中的max_trx_id,说明事务A执行期间,有另外一个事务更新了数据,所以并不能查询到。

图 2-10

然后顺着undo log 版本链条往下找,查询到自己修改过的值。

通过undo log版本链条 + ReadView 这套机制,我们知道了在事务开启之后,只可以读到事务开始之前或者此事务自己修改的数据。这样,就可以实现多个事务并发执行时候的数据隔离。

3. 基于ReadView机制实现的RC隔离级别

已提交读隔离级别,说明在事务运行期间,其他事务执行并且提交了,你就可以都到别的事务更新的数据,所以会出现不可重复读和幻读的问题。

RC隔离级别的核心就是,每次发起一次查询,都重新生成一个ReadView。

假设现在有两个事务对同意一行数据并发执行,分别是事务A和事务B,事务A是查询数据,事务id是50;事务B是更新数据,事务id是70。

现在事务A发起查询,开启一个ReadView。因为事务B是并发执行的,所以ReadView中的结构为:

图 3-1

所以,此时无论事务B如何修改数据并提交事务,事务A都无法读取到事务B修改的值。原因也很简单,事务B的事务id存在ReadView的m_ids的活跃事务列表中。

那如何才能让事务A可以读到事务B更新并提交事务的值呢?

那就是每次查询,都重新开启一个ReadView。

现在假设事务B已经把该行的数据改成了值B,并且提交了。事务A再次进行查询,重启生成一个ReadView。此次生成的ReadView中,数据库内活跃的事务只有事务A了。

图 3-2

此时事务A再次基于ReadView判断,发现这条数据的trx_id=70,虽然在min_trx_id与max_trx_id范围之间,但是并不在m_ids列表内。说明事务B在生成本次ReadView之前已经提交。

因为是在生成ReadView之前就提交的事务,说明事务A就可以查询到事务B修改过得值。从而实现已提交读。

图 3-3

所以,已提交读隔离级别关键的地方在于,每次查询都生成新的ReadView。

4. 基于ReadView机制实现的RR隔离级别

接下来将要讲的是MySQL中的默认隔离级别可重复读,是如何同时避免不可重复读和幻读。

可重复读隔离级别,顾名思义,就是一个事务读同一条数据,无论读多少次,都是同一个值。别的事务就算修改数据提交后,也无法读到它的值。同时,如果别的事务插入一些新的数据,也是无法读到,就可以避免不可重复读和幻读了。

假设数据库已经存在一条数据,此时事务A和事务B同时执行,事务A的id是60,事务B的id是70

图 4-1

此时事务A发起了一个查询,且第一次查询就会生成一个ReadView

图 4-2

事务A基于ReadView去查这条数据,发现trx_id=50是小于min_trx_id的,说明是执行事务A之前就已经提交了的事务插入的,所以可以查到这条数据。

图 4-3

此时事务B过来更新这行数据,把该值改成了值B,同时生成一个undo log,且事务B提交了。

图 4-4

但这个时候,就算事务B已经提交了,在ReadView里面的m_ids中也是存在事务B的事务id,m_ids记录的是执行事务A的时候其他也正在执行的事务的id,并不是说提交了就不存在于m_ids中,除非跟RC隔离级别一样,再生成一个新的ReadView。

所以事务A再次去查询这行数据时,因为m_ids列表中有事务B的事务id,说明事务B也是数据库的活跃事务,就算事务B提交了并不会读取到值B,而实顺着undo log版本链条找到相应的值。

图 4-5

看到这里,就可以清晰的了解到,是如何通过ReadView去避免不可重复读的问题。

那如果是插入数据可能导致的幻读呢?

假设事务A先用”select * from table where id > 10“来查询,此时可能查到的数据只有原始值这条数据。

图 4-6

现在有一个事务C插入了一条数据,然后提交了。

图 4-7

接着事务A再次查询,会发现符合条件的数据有两天,一条是原始值,一条是值C。

但根据ReadView进行判断发现,值C的trx_id=80,比max_trx_id(71)要大。说明是自己发起查询之后,这个事务才开启的,所以此时这条数据是不能查询的。

图 4-8

所以本次查询,事务A还是只能查询到一条数据。

这样看来,在依托ReadView这个机制下,事务A也就不会产生幻读的情况。

看到这里相信大家也就明白基于ReadView机制,RR隔离级别是如何避免不可重复读和幻读的了。

总结

通过一系列篇章和底层原则的分析,大家都明白数据库的脏写、脏读、不可重复读和幻读问题怎样产生的。

而MySQL又是如何基于undo log多版本链条 + ReadView机制 这套机制,实现的RR隔离级别来避免脏写、脏读、不可重复读和幻读问题。