早上上班途中,趁着坐地铁的功夫翻了翻高性能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便失去了意义。抱着存在即为合理的态度,所以觉得自己的理解应该是有问题。
然后我了解到InnoDB为数据库中的每一行添加了三个隐藏字段:DB_TRX_ID(事务版本号)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID(隐藏ID)。
InnoDB基于事务版本号、回滚指针这两个字段,可以在undo log中形成一个单向链表,最新版本的数据放在链表头部,历史数据通过DB_ROLL_PTR指针进行关联。如下图所示
有了这种结构的数据后,InnoDB可以很方便的管理多个版本的数据,也为MVCC的实现打下来基础。
接下来我们来了解一下MVCC在InnoDB中具体的实现逻辑是怎样的,以及MVCC解决了哪些问题。
首先,InnoDB在事务开启后执行第一个查询时,会创建一个快照(下文称之为ReadView),这个ReadView包含了以下信息
紧接着InnoDB会通过查询语句定位到最新版本的数据行,并根据以下规则获取到可以访问的数据版本。
可重复读隔离级别下,ReadView只会在第一次查询时创建,同一个事务中后续所有的查询共用一个ReadView,由此便解决了不可重复读的问题。
读已提交隔离级别下,每次查询都会创建一个新的ReadView。新建的ReadView会更新creator_trx_id以外的其余字段,因此不可重复读现象依然存在。但是由于ReadView可以判断出修改此数据的事务是否已经提交,因此可以避免脏读的出现。
其次,从上述MVCC实现逻辑中可以发现,没有任何加锁、获取锁的操作,因此MVCC读操作不会因为等待锁而阻塞(也就是常说的非阻塞读)。
MVCC可以解决脏读、不可重复读,并且实现了非阻塞读的功能。
读已提交隔离级别:每次读操作都会设置和读取自己的新快照(ReadView)。
可重复读隔离级别:同一个事务共用第一次查询时建立的快照(ReadView)。
最后扩展一个延伸的知识点,其实Mysql中的读操作可以分为两大类:快照读与当前读。
快照读是指通过MVCC实现的非阻塞读,常见的快照读操作如下:
当前读也叫加锁读,每次读取数据都是读取数据的最新版本,并且会对其进行加锁。常见的当前读操作如下
为什么要区分这两种读操作呢?因为MVCC并不能解决幻读的问题。即使是在可重复读级别,通过当前读依然会出现幻读问题。此问题最终是通过间隙锁来解决的。