如何理解MySQL的MVCC多版本并发控制

发布时间 2023-04-03 23:25:37作者: loveletters

前言

我们知道在mysql中存在四种隔离级别(读未提交、读已提交、可重复读、序列化),它默认的就是隔离级别就是可重复读,它能够解决脏读、不可重复读问题,并且在innodb引擎下能部分解决幻读问题。在mysql innodb存储引擎下RC(读已提交),RR(可重复读)基于MVCC(多版本并发控制)进行并发事务控制。那我来了解一下它是如何实现的。

MVCC(多版本并发控制)

MVCC是基于”数据版本“对并发事务进行访问

这里有4个操作ABCD,事务id分别为1、2、3、4

  1. 事务A执行的操作是将id为1088的name更新为张三后提交
  2. 事务B执行的操作在事务A commit后将name更新为张小三后提交
  3. 事务C执行的操作是在事务B执行完成后将name更新为张老三后提交
  4. 事务D执行的两次查询,第一次是在事务B update后 但是还没commit的时候进行的一次查询,第二次是在事务C update后还未commit前进行的一次查询。

针对于D的这两次查询如果是在RR级别(可重复读)第一次跟第二次查询的结果都为张三,如果是在RC级别(读已提交)第一次为张三,第二次为张小三。在RC级别下出现了”不可重复读“

版本链

为什么会出现这种情况呢,其实在innodb模式下有一个基于undo_log的版本链

最上面一行是表中当前数据,而且它会在这条数据上面额外添加两个字段,TRX_ID 最后一次更新的事务ID,DB_ROLL_PTR则是记录者上一次版本变化的那条记录地址。最后一条为最原始的数据,所以没有事务编号跟上次记录的地址。mysql的回滚就是根据undo_log版本链来进行回滚操作。并且mysql在确保版本链数据不再被引用后会进行删除。

ReadView

ReadView是”快照读“SQL执行时MVCC提取数据的依据。快照读就是最普通的select查询语句。而与之相对应的还有一个当前读,指的是执行 insert、update、delete、select ... for update(写锁/排它锁) 、 select ... lock in share mode(读锁/共享锁)

ReadView是一个数据结构,包含四个字段

  1. m_ids:当前活跃的事务编号集合
  2. min_trx_id:最小活跃事务编号
  3. max_trx_id:预分配事务编号,当前最大事务编号+1
  4. creator_trx_id:ReadView创建者的事务编号

在RC模式下,在每一次执行快照读的时候生成ReadView

如图:在第一次快照读的时候因为A已经提交了,所以当前活跃的事务为BCD,m_ids为2,3,4。最小活跃的事务id为2,预分配的事务id为5,创建者的id为4。在进行第二次快照读的时候,事务B已经commit掉了这个时候活跃的事务为CD,那么m_ids为3,4,并且最小的事务id为3。

接下来我们来分析一下MVCC是如何基于ReadView进行数据提取的。版本链数据访问有几个规则:

  1. 判断当前的事务id等于creator_trx_id吗?如果成立说明数据就是这个事务更改的,可以访问。
  2. 判断trx_id<min_trx_id吗?如果成立说明数据已经提交了,可以访问。
  3. 判断trx_id>max_trx_id吗?如果成立说明该事务实在ReadView生成以后才开启的,则不允许访问。
  4. 判断min_trx_id<= trx_id<=max_trx_id?如果成立则在m_ids数据中对比,不存在则代表数据是已经提交过后的,则可以访问。

我们来进行第一次查询分析,第一条数据trx_id为3,而ReadView中的creator_trx_id为4,所以不符合第一个条件。并且trx_id大于min_trx_id(2),所以第二个条件也不符合。并且也不大于max_trx_id(5)所以第三个也不符合,最后虽然它的trx_id(3)大于min_trx_id(2) 小于max_trx_id(5),但是它存在于m_ids(2,3,4)中,则说明这条数据不符合条件然后继续代入下一条数据进行判断。对比下来发现张三这条数据 trx_id 为1符合我们的条件所以返回这条数据。

第二次查询也是一样的,代入ReadView进去对比,发现得到的结果为张小三。

我们通过这张图也可以验证我们推导的结果,第一次的查询返回的是张三这条数据,第二次返回的是张小三的这条数据。

这也验证了在RC模式下,两次读取到的结果不一致,是不可重复读。

可重复读(RR):仅在第一次执行快照读的时候生成ReadView,后续快照读复用。

所以在这种ReadView、版本链、判断规则都没有发生变化的情况下,两次读取到的数据当然是一样的,所以也就不会有不可重复读的问题。

RR级别下使用MVCC能在一定程度上解决幻读的问题,并不能完全解决。因为MVCC并不是采用锁的机制对事务数据做了隔离,而是通过版本控制变相解决幻读的问题。

在连续多次快照读下,ReadView会产生复用,没有幻读的问题。

但是在两次快照读之间存在当前读,并且覆盖到了其他事务变更的数据,ReadView会重新生成,导致幻读的产生。

如图在事务B中第一次查询到只有一条结果,这个时候会生成一个ReadView,然后事务A新插入一条数据后。这个时候事务B执行了update操作,这会产生一次当前读,并且会重新生成ReadView,最后再查询则出现了2条数据,产生了幻读。