MVCC 机制的原理及实现
什么是 MVCC
MVCC(Multiversion Concurrency Control)翻译过来是多版本并发控制,和数据库锁一样,也是一种并发控制的解决方案。
在InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读,而这个读指的就是快照读,而非当前读。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
快照读
所谓快照读,就是读取的是快照数据,即快照生成的那一刻的数据,像我们常用的普通的SELECT语句在不加锁情况下就是快照读:
SELECT * FROM xx_table WHERE ...
注意:快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。
当前读
当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的SELECT,或者对数据进行增删改都会进行当前读:
SELECT * FROM xx_table LOCK IN SHARE MODE; #共享锁
SELECT * FROM xx_table FOR UPDATE; #排他锁
INSERT INTO xx_table values ... #排他锁
DELETE FROM xx_table WHERE ... #排他锁
UPDATE xx_table SET ... #排他锁
解决什么问题
我们知道,在数据库中,对数据的操作主要有2种,分别是读和写,而在并发场景下,就可能出现以下三种情况:
- 读-读并发:不存在任何问题,也不需要并发控制
- 读-写并发:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写并发:有线程安全问题,可能会存在更新丢失问题
在没有写的情况下读-读并发是不会出现问题的,而写-写并发这种情况比较常用的就是通过加锁的方式实现。那么,读-写并发则可以通过MVCC的机制解决。
实现原理
Undo Log
undo log是Mysql中比较重要的事务日志之一,是一种用于回退的日志,在事务没提交之前,MySQL会先记录更新前的数据到undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用undo log来进行回退。
- insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的Undo Log Segment也会被系统回收
- update或delete时产生的undo log,不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
一条记录在同一时刻可能有多个事务在执行,那么undo log会有一条记录的多个快照,那么在这一时刻发生SELECT要进行快照读的时候,要读哪个快照呢?
行记录的隐式字段
其实,数据库中的每行记录中,除了保存了我们自己定义的一些字段以外,还有一些重要的隐式字段的:
- db_row_id:隐藏主键,如果我们没有给这个表创建主键,那么会以这个字段来创建聚簇索引
- db_trx_id:对这条记录做了最新一次修改的事务的ID
- db_roll_ptr:回滚指针,指向这条记录的上一个版本,其实他指向的就是Undo Log中的上一个版本的快照的地址
注意:以上字段只有在聚簇索引的行记录中才会有,而在普通二级索引中是没有这些值的。
每一次记录变更之前都会先存储一份快照到undo log中,那么这几个隐式字段也会跟着记录一起保存在undo log中,就这样,每一个快照中都有一个db_trx_id字段表示了对这个记录做了最新一次修改的事务的ID ,以及一个db_roll_ptr字段指向了上一个快照的地址。(db_trx_id和db_roll_ptr是重点,后面还会用到)
这样就形成了一个快照链表:
图片
有了undo log,又有了几个隐式字段,我们好像还是不知道具体应该读取哪个快照,那怎么办呢?
Read View
Read View 是InnoDB中一个至关重要的概念,是实现MVCC的基础,同时也是支持不同的事务隔离级别的基础,同时提高系统的并发能力和性能。
Read View主要来帮我们解决可见性的问题的, 即他会来告诉我们本次事务应该看到哪个快照,不应该看到哪个快照。
- 在可重复读(Repeatable Read)级别下,快照(Read View)在事务开始后第一次查询时创建一次,并在整个事务期间保持不变。
- 在读已提交(Read Committed)级别下,快照(Read View)会在每次查询时重新创建,以反映数据库中的最新提交更改。
在Read View中有几个重要的属性:
- trx_ids,表示在生成Read View时当前系统中活跃的读写事务的事务id列表。
- low_limit_id,应该分配给下一个事务的id值。
- up_limit_id,未提交的事务中最小的事务ID。
- creator_trx_id,创建这个Read View的事务ID。
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID )取出来,与系统当前其他活跃事务的ID去对比(由Read View 维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID,那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本。
案例
假如一个ReadView的内容为:
trx_ids = [5,6,8)
low_limit_id = 8
up_limit_id = 5
creator_trx_id = 7
假设当前事务要读取某一个记录行,该记录行的db_trx_id(即最新修改该行的事务ID)为 trx_id,那么,就有以下几种情况了:
1、trx_id 2、trx_id>=low_limit_id,即大于8的事务,说明该事务在生成ReadView后才生成,所以该事务的结果就是不可见的。 3、up_limit_id 如果,事务ID在trx_ids列表中,如6,那么表示在当前事务开启时,这个事务还是活跃的,那么这个记录对于当前事务来说应该是不可见的。 如果,事务id不在trx_ids列表中,如7,那么表示的是在当前事务开启之前,其他事务对数据进行修改并提交了,所以,这条记录对当前事务就应该是可见的。 当然这里有个例外情况,那就是这个trx_id=creator_trx_id,那么就肯定是可见的 总结一下就是,一个事务能看到的是在他开始之前就已经提交的事务的结果,而未提交的结果都是不可见的。 当数据的事务ID不符合Read View规则时候,那就需要从undo log里面获取数据的历史快照,然后数据快照的事务ID再来和Read View进行可见性比较,如果找到一条快照,则返回,找不到则返回空。总结
在InnoDB中MVCC就是通过Read View + Undo Log来实现的,undo log中保存了历史快照,而Read View用来判断具体哪一个快照是可见的。