MVCC的全称是Multiversion Concurrency Control(多版本并发控制器),是一种事务隔离级别的无锁的实现方式,用于提高事务的并发性能,即事务隔离级别的一种底层实现方式。
在了解MVCC之前,我们先来回顾一些简单的知识点:数据库的隔离级别和并发场景。
数据库的隔离级别
主要用来解决多个事务在并发的情况下对同一个数据进行读写操作所产生的一些列线程不安全的问题。
- 脏读 -- 读未提交(RU)
- 事务A读取到了事务B还未提交的数据。
- 不可重复读 -- 读已提交(RC)
- 事务A读取到事务B已经提交的数据,可以解决脏读问题,但会出现不可重复读。
- 可重复读(RR)
- 同一是事务下,事务在执行期间,多次读取同一数据时,能够保证读取到的数据是一致的,可以解决不可重复读问题,但在事务读取过程中,如果有另外一个新事务新增/变更,会出现幻读。
- 串行化(serializable)
- 最高的隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读,幻读和不可重复读的问题,但是这种事务隔离级别效率最低,比较耗费数据库性能。
读已提交:
解决脏读问题为什么不使用行锁?如果我们在事务一中对数据进行修改操作时给数据添加一个行锁,那么接下来的事务中想要执行SQL语句进行查询操作,那么该操作将会被阻塞,直到修改操作执行完毕,行锁被解开为止,如此一来,就会降低并发性。
三种并发场景
- 读 - 读
- 不存在线程安全问题,不需要关心并发
- 读 - 写
- 有线程安全问题,可能会存在数据更新丢失的问题,比如第一类更新丢失,第二类更新丢失
第一类更新丢失:事务A回滚时,将已经提交的事务B更新的数据覆盖了
第二类更新丢失:事务A提交覆盖了事务B已经提交的数据,造成事务B所做的操作丢失
什么是MVCC
定义
即多版本并发控制,是一种并发控制的方法,一般在数据库管理系统中实现对数据库的并发访问,在编程语言中实现事务内存。
目的
在最早的数据库系统中只有读 - 读之间可以并发,读 - 写、写 - 写之间都要阻塞。在引入多版本并发控制技术之后,只有写 - 写之间相互阻塞,其他三种操作都可以并行,这样就达到了提高InnoDB引擎并发度的目的。
实现
在MySQL的内部实现中,InnoDB是通过undo log实现,undo log可以找回数据的历史版本。找回的历史版本可以提供给用户读(按照隔离级别定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据,在InnoDB内部中,会记录一个全局的活跃读写事务数据组,其主要用来判断事务的可见行
MVCC的三个关键点
MVCC是如果无锁地实现事务的隔离级别的呢?主要就是靠以下三个关键因素:
隐藏列
在数据库表单中除了我们创建的原数据的列外,数据库帮我们维护的三个看不到的隐藏列:DB_TRX_ID(事务ID),DB_TRX_ID(存储旧的事务的指针),(ROW_ID)
undo log
回滚实现:
通过两个隐藏列trx_id(最近一次提交事务的ID)和roll_pointer(上个版本的地址)建立一个版本链。并在事务中读取的时候生成一个Read View(读视图),在Read Committed隔离级别下,每次读取都会生成一个读视图,而在Repeatable Read隔离级别下,只会在第一次读取时生成一个读视图
ReadView
定义
不加锁的select就是快照读,即不加锁的非阻塞读。(快照读的前提是非serializable隔离级别,在该隔离级别下,快照读会退化为当前读),之所以出现快照读,是基于高并发性能的考虑,快照读的实现是基于MVCC的,可以认为MVCC是行锁的变种,但是他在很多情况下避免了加锁操作,降低了开销。因为多版本的原因,导致快照读可能读取到的不一定是数据的最新版本,而有可能是历史版本
当前读和快照读与MVCC关系
MVCC可以理解为是一个"维护数据的多个版本,使得读写操作没有冲突"的概念,是一种理想状态。而在MySQL中,快照读,就是实现MVCC理想模型的其中一个具体的非阻塞读功能,而相对而言,当前读就是一个悲观锁的具体功能实现
具体实现&&判断顺序
存储三条数据:creator_trx_id(当前事务ID),min_trx_id(当前未提交的事务的ID),max_trx_id(未开始的事务)
- 首先通过undolog拿到最新版本的数据,最新一次修改本条数据的事务ID
- 第一次判断:将当前事务ID与最新一次修改本条数据的事务ID进行比较
- 二者相等-->本条版本的数据是在当前事务中保存的-->该条数据可以读取
- 二者不相等则不能直接拿到该值,继续进行判断
- 第二次判断:比较最新一次修改本条数据的事务ID和最小的事务ID
- 小于-->当前版本数据是在事务开始之前保存的-->该条数据可以读取到
- 大于等于-->不能直接拿到该值,继续进行判断
- 第三次判断:比较最新一次修改本条数据的事务ID和最大的事务ID
- 大于-->当前版本的数据是在本事务开始之后保存的-->本数据不能被读取-->顺着指针找到上一个版本的数据
- 第四次判断:比较最新一次修改本条数据的事务ID是否在最小事务ID和最大事务ID之间
- 存在于二者之间-->当前版本的数据是在未提交的并发事务中
- 如果事务隔离级别是读已提交-->该条数据不能被读取
- 不存在于二者之间-->沿着指针找到上一个版本的数据,再进行以上四次判断
- 存在于二者之间-->当前版本的数据是在未提交的并发事务中