MySQL中的MVCC实现机制
一、什么是MVCC?
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种无锁的并发控制方法,一般在数据库管理系统中,用于实现对数据库的并发访问。
我们知道,在数据库中,对数据的操作主要有2中,分别是读和写,而在并发场景下,就可能出现以下三种情况:
- 读-读并发
- 读-写并发
- 写-写并发
我们都知道,在没有写的情况下发读-读并发是不会出现问题的,而写-写并发这种情况比较常用的就是通过加锁的方式实现。那么,读-写并发则可以通过MVCC的机制解决。MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。本文就来介绍下一下MySQL中MVCC的实现机制。
二、快照读和当前读
要想搞清楚MVCC的机制,最重要的一个概念那就是快照读。
1. 快照读
所谓快照读,就是读取的是快照数据,即快照生成的那一刻的数据,像我们常用的普通的SELECT语句在不加锁情况下就是快照读。如:
SELECT * FROM xx_table WHERE ...
2. 当前读
和快照读相对应的另外一个概念叫做当前读,当前读就是读取最新数据,所以,加锁的 SELECT,或者对数据进行增删改都会进行当前读,比如:
SELECT * FROM xx_table LOCK IN SHARE MODE; SELECT * FROM xx_table FOR UPDATE; INSERT INTO xx_table ... DELETE FROM xx_table ... UPDATE xx_table ...
可以说快照读是MVCC实现的基础,而当前读是悲观锁实现的基础。
那么,快照读读到的快照是从哪里读到的的呢?换句话说,快照是存在哪里的呢?
三、undo log
undo log是Mysql中比较重要的事务日志之一,顾名思义,undo log是一种用于回退的日志,在事务没提交之前,MySQL会先记录更新前的数据到 undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undo log来进行回退。
这里面提到的存在 undo log 中的”更新前的数据”就是我们前面提到的快照。所以,这也是为什么说 undo log 是MVCC实现的重要手段的原因。
那么,一条记录在同一时刻可能有多个事务在执行,那么,undo log会有一条记录的多个快照,那么在这一时刻发生SELECT要进行快照读的时候,要读哪个快照呢?
这就需要用到另外几个信息了
四、隐式字段
其实,数据库中的每行记录中,除了保存了我们自己定义的一些字段以外,还有一些重要的隐式字段的:
1. db_row_id
隐藏主键,如果我们没有给这个表创建主键,那么会以这个字段来创建聚簇索引。
2. db_trx_id
对这条记录做了最新一次修改的事务的ID(6字节)
3. db_roll_ptr
回滚指针(7字节),指向这条记录的上一个版本,其实他指向的就是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 登场了。Read View 主要来帮我们解决可见性的问题的, 即他会来告诉我们本次事务应该看到哪个快照,不应该看到哪个快照。
1. 在 Read View 中有几个重要的属性
1) trx_ids,系统当前未提交的事务 ID 的列表。
2) low_limit_id,未提交的事务中最大的事务 ID。
3) up_limit_id,未提交的事务中最小的事务 ID。
4) creator_trx_id,创建这个 Read View 的事务 ID。
每开启一个事务,我们都会从数据库中获得一个事务 ID,这个事务 ID 是自增长的,通过 ID 大小,我们就可以判断事务的时间顺序。
2. 那么,一个事务应该看到哪些快照,不应该看到哪些快照该如何判断呢?
其实原则比较简单,那就是事务ID大的事务应该能看到事务ID小的事务的变更结果,反之则不能!
我们前面说过,每一条记录上都有一个隐式字段db_trx_id记录对这条记录做了最新一次修改的事务的ID。那么接下来,数据库会拿这条记录db_trx_id和Read View进行可见性比较。
1) db_trx_id < up_limit_id
这种情况说明,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见.
2) db_trx_id>=low_limit_id
这种情况说明,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见(不可见怎么办呢?后面讲)
3) up_limit_id < = db_trx_id < low_limit_id
这种情况下,会再拿db_trx_id和Read View中的trx_ids进行逐一比较。
如果db_trx_id 在trx_ids列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
如果db_trx_id不在trx_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
所以,当读取一条记录的时候,经过以上判断,发现记录对当前事务可见,那么就直接返回就行了。那么如果不可见怎么办?没错,那就需要用到undo log了。
当数据的事务ID不符合Read View规则时候,那就需要从undo log里面获取数据的历史快照,然后数据快照的事务ID再来和Read View进行可见性比较,如果找到一条快照,则返回,找不到则返回空。
所以,总结一下,在InnoDB中,MVCC就是通过Read View + Undo Log来实现的,undo log中保存了历史快照,而Read View 用来判断具体哪一个快照是可见的。
六、MVCC和隔离级别
其实,根据不同的事务隔离级别,Read View的获取时机是不同的,在RC(Read Commited)下,一个事务中的每一次SELECT都会重新获取一次Read View,而在RR( Repeatable Read)下,一个事务中只在第一次SELECT的时候会获取一次Read View。
所以,可重复读这种事务隔离级别之下,因为有MVCC机制,就可以解决不可重复读的问题,因为他只有在第一次SELECT的时候才会获取一次Read View,天然不存在重复读的问题了。
七、总结
总之,MVCC就是因为设计者不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案,所以在数据库中,因为有了MVCC,所以我们可以形成两个组合:
1. MVCC + 悲观锁 MVCC解决读写冲突,悲观锁解决写写冲突
2. MVCC + 乐观锁 MVCC解决读写冲突,乐观锁解决写写冲突
这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题
参考链接:
https://www.51cto.com/article/719614.html
https://xiaolincoding.com