MVCC全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是为MySQL并发场景下无锁生成读视图进行读操作来进行多版本控制。
MySQL中InnoDB中实现了事务(MVCC+锁),其中通过MVCC解决隔离性问题。具体而言,MVCC就是为了实现读-写冲突不加锁。
ACID模型是一组数据库设计原则,强调对业务数据和关键任务应用程序的重要可靠性。
如果您有额外的软件保护、超可靠的硬件或可以容忍少量数据丢失或不一致的应用程序,您可以调整
MySQL设置牺牲部分ACID可靠性以获得更高的性能或吞吐量。
事务隔离是数据库处理的基础之一。当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。
隔离级别是在多个事务同时进行更改和执行查询时,有效平衡性能与可靠性、一致性和结果的可再现性的设置。
在谈隔离级别之前,首先要知道,隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。
SQL 标准的事务隔离级别包括:
- 只有在隔离级别为读未提交(RU)时,会出现脏读。
- RR隔离级别下需要利用间隙锁来解决幻读问题
MVCC(多版本并发控制)就是为了实现读-写冲突不加锁,而这个读指的就是快照读。当前读实际上是一种加锁的操作,是悲观锁的实现
当前读
当前读读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。如共享锁select for share, 排他锁select for update,update,insert,delete等操作
快照读
快照读是不加锁的非阻塞读,如普通的select操作。之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销
快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读
Read View;Read View, 之后的快照读获取的都是同一个Read View| session A | session B |
|---|---|
| begin; | begin; |
| select nick from t where id=1; (快照读 nick=赵一) | |
| update t set nick=‘钱二’ where id=1; commit; | |
| select nick from t where id=1; (快照读 nick=钱二) | |
| select nick from t where id=1 for share; (当前读 nick=钱二) |
| session A | session B |
|---|---|
| begin; | begin; |
| select nick from t where id=1; (快照读 nick=赵一) | |
| update t set nick=‘钱二’ where id=1; commit; | |
| select nick from t where id=1; (快照读 nick=赵一) | |
| select nick from t where id=1 for share; (当前读 nick=钱二) |
在RR级别,事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读的地方非常关键,它有决定该事务后续快照读结果的能力
begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。
第一种启动方式,一致性视图是在第执行第一个快照读语句时创建的; 第二种启动方式,一致性视图是在执行 start transaction with consistent snapshot 时创建的。
MVCC为多版本并发控制,目的是为了解决读写冲突。它的实现原理主要是依赖记录中的3个隐式字段,undo log,Read View来实现的。
MySQL行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
DB_ROW_ID 6byte, 隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引DB_TRX_ID 6byte, 最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务IDDB_ROLL_PTR 7byte, 回滚指针,指向这条记录的上一个版本(存储于rollback segment里)DELETED_BIT 1byte, 记录被更新或删除并不代表真的删除,而是删除flag变了
如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键;DB_TRX_ID是当前操作该记录的事务ID; 而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本
此段原文修改自MySQL InnoDB的MVCC实现机制
著作权归https://pdai.tech所有。
undo log是一种用于撤销回退的日志,在事务没提交之前,MySQL会先记录更新前的数据到 undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undo log来进行回退。
undo log主要分为3种:
Insert undo log:插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值Delete undo log:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中
undo log的存储由InnoDB存储引擎实现,数据保存在InnoDB的数据文件中。在InnoDB存储引擎中,undo log是采用分段(segment)的方式进行存储的。rollback segment称为回滚段,每个回滚段中有1024个undo log segment。
在MySQL5.5之前,只支持1个rollback segment,也就是只能记录1024个undo操作。在MySQL5.5之后,可以支持128个rollback segment
undo log实际上就是存在rollback segment中旧记录链,对MVCC有帮助的实质是update undo log,它的执行流程如下:
比如一个有个事务插入persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL

现在来了一个事务1对该记录的name做出了修改,改为Tom


从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录
此段原文修改自MySQL 实战 45 讲
InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
Read View没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”
按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。
在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。
InnoDB 里面每个事务有一个唯一的事务ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务ID(
DB_TRX_ID)
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)

数据版本的可见性规则,就是基于数据的DB_TRX_ID和这个一致性视图的对比结果得到的
对于当前事务的启动瞬间来说,一个数据版本的DB_TRX_ID,根据上图对比可知,有以下几种可能:
DB_TRX_ID在数组中,表示这个版本是由还没提交的事务生成的,不可见;DB_TRX_ID不在数组中,表示这个版本是已经提交了的事务生成的,可见。InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。
接下来,我们继续看一下图中的三个事务,分析下事务 A 的语句返回的结果,为什么是 k=1。
| 事务A | 事务B | 事务C |
|---|---|---|
| start transaction with consistent snapshot; | ||
| start transaction with consistent snapshot; | ||
| update t set k=k+1 where id=1; | ||
| update t set k=k+1 where id=1; select k from t where id=1; | ||
| select k from t where id=1; commit; | ||
| commit; |
这里,我们不妨做如下假设:
这样,事务 A 的视图数组就是 [99,100], 事务 B 的视图数组是 [99,100,101], 事务 C 的视图数组是 [99,100,101,102]。
为了简化分析,我先把其他干扰语句去掉,只画出跟事务 A 查询逻辑有关的操作:

从图中可以看到,第一个有效更新是事务 C,把数据从 (1,1) 改成了 (1,2)。这时候,这个数据的最新版本的DB_TRX_ID是 102,而 90 这个版本已经成为了历史版本。
第二个有效更新是事务 B,把数据从 (1,2) 改成了 (1,3)。这时候,这个数据的最新版本(即DB_TRX_ID)是 101,而 102 又成为了历史版本。
你可能注意到了,在事务 A 查询的时候,其实事务 B 还没有提交,但是它生成的 (1,3) 这个版本已经变成当前版本了。但这个版本对事务 A 必须是不可见的,否则就变成脏读了。
好,现在事务 A 要来读数据了,它的视图数组是 [99,100]。当然了,读数据都是从当前版本读起的。所以,事务 A 查询语句的读数据流程是这样的:
DB_TRX_ID=101,比高水位大,处于红色区域,不可见;DB_TRX_ID=102,比高水位大,处于红色区域,不可见;DB_TRX_ID=90,比低水位小,处于绿色区域,可见。这样执行下来,虽然期间这一行数据被修改过,但是事务 A 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读。
这个判断规则是从代码逻辑直接转译过来的,但是正如你所见,用于人肉分析可见性很麻烦。
所以,我来给你翻译一下。一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:
现在,我们用这个规则来判断图 4 中的查询结果,事务 A 的查询语句的视图数组是在事务 A 启动的时候生成的,这时候:
参考资料: