前言
早上上班途中,趁着坐地铁的功夫翻了翻高性能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并不能解决幻读的问题。即使是在可重复读级别,通过当前读依然会出现幻读问题。此问题最终是通过间隙锁来解决的。