目录
概述
MVCC(Multi-Version Concurrency Control 多版本并发控制),是一种不利用锁机制实现的隔离级别,主要实现了在保证数据的一致性的前提下,实现了读写的并行。从而大大提高数据库系统的并发性能。
数据库并发场景有三种,分别为:
- 读-读:不存在任何问题,也不需要并发控制
- 读-写:有线程安全问题,可能会造成事务隔离性问题
- 写-写:有线程安全问题,可能会存在更新丢失问题
引入了MVCC技术可以为数据库解决以下问题
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
- 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
这样大幅度提高了并发度。这也是为什么现阶段,几乎所有的RDBMS(关系型数据库管理系统),都支持了MVCC。
了解MVCC技术前,我们先了解下MySQL InnoDB的读操作是如何的?
MySQL InnoDB下读操作分类
在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。而当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
一、快照读(snapshot read)
读取的是记录的可见版本 (有可能是历史版本),不用加锁。
简单的select操作,属于快照读,不加锁:
SELECT * FROM TABLE WHERE XXXX;
当执行SELECT之后INNODB默认会执行快照读,相当于就是给你目前的状态找了一张照片,以后执行select 的时候就会返回当前照片里面的数据,当其他事务提交了也对你不造成影响,和你没关系,这就实现了可重复读了,那这个照片是什么时候生成的呢?不是开启事务的时候,是当你第一次执行select的时候,也就是说,当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据......之后无论再有其他事务commit都没有关系,因为照片已经生成了,而且不会再生成了,以后都会参考这张照片。
二、当前读(current read)
当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
SELECT * FROM TABLE WHERE XXX LOCK IN SHARE MODE;
SELECT * FROM TABLE WHERE XXX FOR UPDATE;
INSERT INTO TABLE VALUES (…);
UPDATE TABLE SET XXX WHERE XXX;
DELETE FROM TABLE WHERE XXX;
当执行上述几个操作的时候默认会执行当前读,也就是会读取最新的记录,也就是别的事务提交的数据你也可以看到,这样很好理解啊,假设你要update一个记录,另一个事务已经delete这条数据并且commit了,这样不是会产生冲突吗,所以你update的时候肯定要知道最新的信息啊。
三、当前读,快照读和MVCC的关系
- 准确的说,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。仅仅是一个理想概念
- 而在MySQL中,实现这么一个MVCC理想概念,我们就需要MySQL提供具体的功能去实现它,而快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现
- 要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC模型在MySQL中的具体实现则是由 3个隐式字段,undo日志版本链 ,Read View 等去完成的,具体可以看下面的MVCC实现原理。
Mysql MVCC实现原理
一、InnoDB引擎的隐式字段
InnoDB会为数据默认创建三个隐式字段,DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR。含义如下所示:
- DB_ROW_ID:隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。
- DB_TRX_ID:事务ID,用来标识最近的一次对本条数据更新的一个事务ID
- DB_ROLL_PTR:回滚指针,用来进行回滚的。
如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本。
二、undo日志版本链
最后我们来举个例子让我们更好理解上面的内容。
比如我们有如下表:
mysql底层实际存储格式如下
比如现在有个事务id是11的执行的这条记录的修改语句
UPDATE MVCC_TEST SET SALARY = 200 WHERE ID = 1
底层数据格式变化如下
比如现在有个事务id是12的执行的这条记录的修改语句
UPDATE MVCC_TEST SET SALARY = 300 WHERE ID = 1
底层数据格式变化如下
三、Read View
ReadView说白了就是一个数据结构,在SQL开始的时候被创建。这个数据结构中包含了3个主要的成员:ReadView{low_trx_id, up_trx_id, trx_ids},在并发情况下,一个事务在启动时,trx_sys链表中存在部分还未提交的事务,那么哪些改变对当前事务是可见的,哪些又是不可见的,这个需要通过ReadView来进行判定,首先来看下ReadView中的3个成员各自代表的意思。
- low_trx_id表示该SQL启动时,当前事务链表中最大的事务id编号,也就是最近创建的除自身以外最大事务编号;
- up_trx_id表示该SQL启动时,当前事务链表中最小的事务id编号,也就是当前系统中创建最早但还未提交的事务;
- trx_ids表示所有事务链表中事务的id集合。
四、可见性比较算法
前面提到了事务是如何实现隔离,就是通过这个可见性比较算法来进行事务隔离的,让当前事务只能读到当前事务沿着undo log链应该读到的数据。
在这里记录下RR级别下的比较算法,其实RC级别下的比较算法和它是一样的,我们暂且描述一次:
首先,我们会有一个trx_id_current表示该行最稳定的事务ID。
当我们开启一个新事务去读取改行记录的时候,InnoDB会创建一个read view(快照),它维护了当前活跃的事务id(也就是未提交的事务),我们姑且称那个最早的事务ID为up_trx_id,最晚的事务ID为low_trx_id。
接下来我们会根据这几个值进行描述比较算法:
- 当trx_id_current < up_trx_id的时候,说明该行数据的稳定事务id小于活跃事务id的,所以该行数据是可见的,跳到步骤五
- 当trx_id_current > low_trx_id的时候,说明该行数据的稳定事务id大于当前快照下的活跃事务ID,所以说该行数据是不可见的,跳到步骤四。
- 当up_trx_id < trx_id_current < low_trx_id,在这个范围的事务ID,那就在read view里的活跃事务ID中遍历,如果trx_id_current == 其中一个事务ID,那么就说明改行数据不可见。跳到步骤四。否则表示可见。
- 从该行记录的回滚指针(DB_ROLL_PIR)指向的下一个最新的事务ID,将这个值赋值给trx_id_current,继续从步骤1开始。
- 将该可见行的数据返回。
用一张图来解释MySQL中的MVCC实现如下所示:
结论
数据库需要做好版本控制,防止不该被事务看到的数据(例如还没提交的事务修改的数据)被看到。在InnoDB中,MVCC技术主要是通过使用readview的技术来实现判断。查询出来的每一行记录,都会用readview来判断一下当前这行是否可以被当前事务看到,如果可以,则输出,否则就利用undolog来构建历史版本,再进行判断,知道记录构建到最老的版本或者可见性条件满足。
MVCC的优点是在大多数情况下代替了行锁,实现了对读的非阻塞,读不加锁,读写不冲突。
MVCC的缺点是每行记录都需要额外的存储空间,需要做更多的行维护和检查工作。
MVCC机制极大的提高了数据库的并发处理能力,所以目前主流的数据库都有通过不同的手段来实现这一机制来提升性能。
参考资料
- http://mysql.taobao.org/monthly/2017/12/01/
- http://mysql.taobao.org/monthly/2017/10/01/
- https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html
- https://zhuanlan.zhihu.com/p/40208895
![](https://img-blog.csdnimg.cn/2020090618421047.jpg)
微信公众号名称:技术茶馆
微信公众号ID : Night_ZW