深入了解Mysql的MVCC机制

深入了解Mysql的MVCC机制

敲得码黛 1,404 2021-10-28

前言

早上上班途中,趁着坐地铁的功夫翻了翻高性能mysql这本书,准备回顾一下MVCC这块的知识点,因为书中对MVCC的讲解不是很多,于是我很快便看完了这一段落,但是文章末尾有一段话引起了我的思考。

MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容,因为READ UNCOMMITED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。

摘抄——《高性能mysql第三版》

之前我对RR与RC的区别不是很清晰,自从了解了MVCC后,因为MVCC机制解决了不可重复读的问题,于是我便认为RR=RC+MVCC。但是书中既然说MVCC可以工作在这两个级别下,那么很显然,我的理解是存在着一些问题的。

思考逻辑:既然MVCC可以工作在RC级别下,那么RC便可以通过MVCC实现重复读,这样一来RR便失去了意义。抱着存在即为合理的态度,所以觉得自己的理解应该是有问题。

MVCC在InnoDB中的实现

然后我了解到InnoDB为数据库中的每一行添加了三个隐藏字段:DB_TRX_ID(事务版本号)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID(隐藏ID)。

  • DB_TRX_ID:记录了创建/更新这条数据的事务版本号(版本号会递增)。
  • DB_ROLL_PTR:记录了一个指向undo log中历史版本的数据指针。(用来支持回滚操作)
  • DB_ROW_ID:一个自增的隐藏行ID。

InnoDB基于事务版本号、回滚指针这两个字段,可以在undo log中形成一个单向链表,最新版本的数据放在链表头部,历史数据通过DB_ROLL_PTR指针进行关联。如下图所示

有了这种结构的数据后,InnoDB可以很方便的管理多个版本的数据,也为MVCC的实现打下来基础。

MVCC解决了哪些问题?

接下来我们来了解一下MVCC在InnoDB中具体的实现逻辑是怎样的,以及MVCC解决了哪些问题。

首先,InnoDB在事务开启后执行第一个查询时,会创建一个快照(下文称之为ReadView),这个ReadView包含了以下信息

  • m_ids: 活动事务id列表(活动事务指的是已经开始、尚未提交/回滚的事务)
  • min_trx_id: 最小活动事务id
  • max_trx_id:最大活动事务id
  • creator_trx_id:当前事务id

紧接着InnoDB会通过查询语句定位到最新版本的数据行,并根据以下规则获取到可以访问的数据版本。

  • 如果被访问版本的trx_id,与readview中的creator_trx_id值相同,表明当前事务在访问自己修改过的记录,直接返回该版本的数据;

  • 如果被访问版本的trx_id,小于readview中的min_trx_id值,表明生成该版本的事务在当前事务生成readview前已经提交,直接返回该版本的数据;

  • 如果被访问版本的trx_id,大于或等于readview中的max_trx_id值,表明生成该版本的事务在当前事务生成readview后才开启,此时该版本不可以被当前事务访问,需要通过隐藏的回滚指针从undo log中读取历史版本;

  • 如果被访问版本的trx_id,在readview的min_trx_id和max_trx_id之间,则需要判断trx_id值是否在m_ids列表中?

    • 如果在:说明readview创建时,创建该版本数据的事务还未提交,因此需要通过回滚指针读取历史版本并返回。
    • 如果不在:说明readview创建时,创建该版本数据的事务已经提交,所以直接返回该版本的数据;

可重复读隔离级别下,ReadView只会在第一次查询时创建,同一个事务中后续所有的查询共用一个ReadView,由此便解决了不可重复读的问题。

读已提交隔离级别下,每次查询都会创建一个新的ReadView。新建的ReadView会更新creator_trx_id以外的其余字段,因此不可重复读现象依然存在。但是由于ReadView可以判断出修改此数据的事务是否已经提交,因此可以避免脏读的出现。

其次,从上述MVCC实现逻辑中可以发现,没有任何加锁、获取锁的操作,因此MVCC读操作不会因为等待锁而阻塞(也就是常说的非阻塞读)。

总结

MVCC可以解决脏读、不可重复读,并且实现了非阻塞读的功能。

读已提交隔离级别:每次读操作都会设置和读取自己的新快照(ReadView)。

可重复读隔离级别:同一个事务共用第一次查询时建立的快照(ReadView)。

当前读与快照读

最后扩展一个延伸的知识点,其实Mysql中的读操作可以分为两大类:快照读当前读

快照读是指通过MVCC实现的非阻塞读,常见的快照读操作如下:

  • select xxx from xxx

当前读也叫加锁读,每次读取数据都是读取数据的最新版本,并且会对其进行加锁。常见的当前读操作如下

  • select xxx from xxx lock in share mode (共享锁/读锁)
  • select xxx from xxx for update (排它锁/写锁)
  • update 、delete、insert

为什么要区分这两种读操作呢?因为MVCC并不能解决幻读的问题。即使是在可重复读级别,通过当前读依然会出现幻读问题。此问题最终是通过间隙锁来解决的。


# mysql # mvcc