一、MVCC的由来

我们知道:事务是具备ACID特性的。但是对于【I】隔离性而言,可以根据不同的场景,权衡使用不同的隔离级别。在MySQL中实现了SQL标准中的四大隔离级别,同时对应了可能会出现的不同问题:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED(读未提交) 可能 可能 可能
READ COMMITTED(读已提交) 不可能 可能 可能
REPEATABLE READ(可重复读) 不可能 不可能 *可能
SERIALIZABLE(可串行化) 不可能 不可能 不可能

其中,需要特别注意的一点是:也是与SQL标准规定不一样的一个地方,MySQL中RR可重复读的隔离级别下,是基本上不会出现幻读问题(这个地方会出现一些特殊的场景,后面会介绍)。

访问MySQL数据一般就是增删改查操作,对应的是insert、delete、update和select语句。

对于insert、delete、update这三种类型的语句而言,需要先定位到最新的数据;对于select语句,也有select … lock in share mode(MySQL8.0中增加了SELECT … FOR SHARE)/select… for update的操作,同样需要读取最新的数据,同时必须堵塞其他并发事务修改当前记录,所以需要进行加锁。 这些操作在MySQL中统一被称为——锁定读Locking Reads),又称为当前读。

而对于普通的select语句,MySQL则根据不同的隔离级别,进行不同的处理:

  • READ UNCOMMITTED(读未提交):读取最新的记录
  • READ COMMITTED(读已提交):读取最新提交事务的记录
  • REPEATABLE READ(可重复读):读取当前事务在开始前的记录或者被自己事务修改的记录
  • SERIALIZABLE(可串行化):读取最新的记录,同时也加上读锁,就是select … lock in share mode

可以看到,对于READ COMMITTED(读已提交)和 REPEATABLE READ(可重复读)这两种隔离级别来说,普通的select是不需要加锁的,并且受到事务发生时机的影响。可以利用事务保存的历史版本数据,控制版本访问权限,这种方案就称之为——MVCC(Multi-Version Concurrency Control),而MVCC下的select查询,被称为——非锁定读(Consistent Nonlocking Reads),又称为快照读。

生成的快照就被称之为ReadView(一致性视图)

利用MVCC提供的快照读能力,可以实现在不加锁的情况下,解决读写冲突时的堵塞问题,极大提升并发查询性能,同时也保证了事务的隔离性。这是在并发性能和一致性之间做的一次trade-off。

二、MVCC的实际应用

前面提到,MVCC是在RC和RR隔离级别下使用历史版本数据实现事务隔离性的,下面我们来看下具体的案例分析。

首先先创建表 t1:

CREATE TABLE `t1` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; #插入一条数据
INSERT INTO `t1` (`id`, `c`, `d`) VALUES ('5', '5', '5');

RR级别场景

事务隔离级别为RR:

SELECT @@tx_isolation

REPEATABLE-READ

1.当事务1执行语句:start transaction with consistent snapshot;会创建一个ReadView,最后进行查询id=5的时候,仍然会读取到快照时的数据,也就是(5,5,5)这条数据



2.事务2同样使用start transaction with consistent snapshot开启了一个ReadView,但是由于事务中使用了update语句更新了这条数据,所以最后事务2查询id=5的数据时,结果是(5,2,5)

RC级别场景

事务隔离级别为RC:

set session transaction isolation level read committed;

SELECT @@tx_isolation

READ-COMMITTED

同样是对于这个案例:

  1. 对于事务2,查询id=5的结果不变,还是(5,2,5)
  2. 对于事务1,由于隔离级别是 READ COMMITTED,可以查询到其他事务已提交的记录,也就是说ReadView是在查询语句开始时才生成的,不是事务开始前,所以查询id=5的结果也是(5,2,5)



    小结一下:
  • RR级别下,MVCC的一致性视图(快照)是只在事务开始(第一次查询语句)时生成;
  • RC级别下,MVCC的一致性视图(快照)是在每次查询语句前时生成;

三、MVCC的实现

3.1 多版本的数据从哪里来——Undo Log

An undo log is a collection of undo log records associated with a single read-write transaction. An undo log record contains information about how to undo the latest change by a transaction to a clustered index record. If another transaction needs to see the original data as part of a consistent read operation, the unmodified data is retrieved from undo log records.

undo log是与单个读写事务相关联的撤消日志记录的集合。undo log记录可以明确如何撤消事务对聚集索引记录的最新更改。如果另一个事务需要通过一致性读取操作查看原始数据,可以从undo log记录中查询未修改前的数据。

也就是说,undo log是为了记录如何回滚事务而生成的。数据每进行一次增删改,就会对应一条或多条undo log。 innoDB中使用不同的undo log类型对insert、delete和update进行区分。

3.1.1 插入操作对应的undo log

TRX_UNDO_INSERT_REC:是插入操作对应的undo log类型,因为插入操作对应的撤销逻辑是删除,所以只需要把这条记录的主键id记录下来。

3.1.2 删除操作对应的undo log

由于MVCC的存在,被删除的记录并不会被真正的删除, 而是进行delete mark操作——只是将行记录上的删除标记位delete_flag改为1,后面在通过purge操作把记录加入垃圾链表中,待后续进行空间复用。生成删除语句对应的undo log类型为TRX_UNDO_DELETE_MARK_REC,相比于TRX_UNDO_INSERT_REC,TRX_UNDO_DELETE_MARK_REC不仅保存了主键id,也保存了相关的索引列信息,用来对删除过程中一些中间状态的清理。

3.1.3 更新操作对应的undo log

更新操作对应的undo log除了会记录主键和索引列信息之外,还会把被更新前各个字段的信息记录下来,还有指向旧记录的DB_ROLL_PTR和DB_TRX_ID。

  • 不更新主键,且被更新的列存储空间不发生变化:直接在原有行记录上面更新。
  • 不更新主键,且被更新的列存储空间发生了变化:先删除旧记录(不是delete mark,而是直接删除),再插入新记录。
  • 更新主键:旧记录进行delete mark,再插入新记录。
  • 过程中更新了二级索引:对旧的二级索引进行delete mark,插入新的二级索引记录。

通过以上3种不同操作对应的undo log,可以得到事务开始前的的历史记录。那么这些记录是如何关联起来的呢?

3.2 旧版本如何关联——行记录隐藏字段和版本链

对于InnoDB引擎的表,聚簇索引记录中包含了两个必要的隐藏字段:

  • DB_TRX_ID:一个事务每次对某条聚簇索引记录进行改动(插入、更新或删除)时,会把该事务id赋值给该字段
  • DB_ROLL_PTR:每次对某条聚簇索引记录进行改动时,会把旧版本写入undo日志中,DB_ROLL_PTR就是指向这条记录上一个版本的指针。

还是对于表 t1:

CREATE TABLE `t1` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

1.插入一条数据

INSERT INTO `t1` (`id`, `c`, `d`) VALUES ('5', '5', '5');

此时版本只有一个:

2.执行了3次变更sql后:

update t1 set c=4 where id=5;
update t1 set c=3 where id=5;
update t1 set c=3 where id=5;

对应的undo版本链就变成:



结合前面的undo log来看,每次进行记录变更的时候,都会把旧值放到一条undo log里面,同时利用DB_ROLL_PTR把多个版本的undo log组成一个版本链,并保持每个版本对应的事务id。

那么如何判断版本链中哪个才是当前事务可见的正确版本呢?

3.3 如何正确访问不同版本数据——Read View访问规则

3.3.1 Read View

Read View就是事务进行快照读操作的时候生成的,是一个逻辑层面的概念。

Read View主要包含以下几部分内容:

  • m_ids: 表示生成Read View时,系统中正在活跃的事务id列表。
  • up_limit_id:表示生成Read View时,系统中正在活跃的最小事务id,也就是m_ids中的最小值。
  • low_limit_id:表示生成Read View时,系统还未分配的下一个事务 ID ,也就是目前已出现过的事务 ID 的最大值 + 1。
  • creator_trx_id:生成Read View的事务id

判断逻辑如下:

  1. 若版本数据的DB_TRX_ID属性值和creator_trx_id相同,代表是本事务修改的记录,可以访问。
  2. 若版本数据的DB_TRX_ID属性值小于up_limit_id,代表是在生成Read View前就已经提交的事务,可以访问。
  3. 若版本数据的DB_TRX_ID属性值大于low_limit_id,代表是在生成Read View之后才开启的事务,不可以访问。
  4. 若版本数据的DB_TRX_ID属性值在up_limit_id和low_limit_id之间,需要再判断DB_TRX_ID是否在m_ids里面,如果在,则代表是在生成Read View时,该版本数据所在的事务还未提交,不可以访问;如果不在,则代表是在生成Read View时,该版本数据所在的事务已经提交,可以访问。

3.3.2 覆盖索引下的MVCC

由于DB_TRX_ID和DB_ROLL_PTR都是在聚簇索引记录中的,当事务中使用覆盖索引进行查询时,如何能确认正确的数据版本呢?

二级索引页上有一个PAGE_MAX_TRX_ID的属性,记录该页面变更时的最大事务id,如果PAGE_MAX_TRX_ID的值是在Read View创建前提交的,那么该页的全部索引都可见;如果不是,则需要根据二级索引的主键进行回表,进行再次判断。

小结一下:

对于一个 Read View 来说,除了自己的更新总是可见以外,有三种情况:

  • 事务未提交的记录版本,不可见;
  • 事务已提交的记录版本,但是是在Read View创建后提交的,不可见;
  • 版本已提交,而且是在Read View创建前提交的,可见。

再结合前文提到的:

  • RR级别下,MVCC的一致性视图(快照)是只在事务开始(第一次查询语句)时生成;
  • RC级别下,MVCC的一致性视图(快照)是在每次查询语句前时生成;

就能找到正确的版本数据,实现RR和RC事务隔离级别的快照读机制。

四、扩展一——MVCC的多版本是否影响性能

因为MVCC是通过回溯undo log的方式,将符合访问条件的历史版本查询出来,而undo log文件是保存在磁盘中,是否会影响查询性能?

由于undo log是为了回滚未成功提交的事务和MVCC存在的,如果事务成功提交,以及当系统里最早的Read View不再访问它们的时候,就会使用purge操作进行清除,所以undo log的生存周期一般来说不会太长,但是使用时,我们仍需要避免使用长事务。

五、扩展二——RR级别能否完全避免幻读问题

回到最开始提到的:MySQL中RR可重复读的隔离级别下,是基本上不会出现幻读问题。但是是否可以完全解决幻读问题呢?

在RR可重复读级别下, InnoDB使用了MVCC解决【快照读】场景下的幻读问题,而在【当前读】场景下, 是使用gap lock间隙锁的方式解决幻读问题。

下面还是使用表 t1 模拟一个案例,在RR级别下开启两个事务会话:

CREATE TABLE `t1` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `codemavs`.`t1` (`id`, `c`, `d`) VALUES ('0', '0', '0');
INSERT INTO `codemavs`.`t1` (`id`, `c`, `d`) VALUES ('1', '1', '1');
INSERT INTO `codemavs`.`t1` (`id`, `c`, `d`) VALUES ('5', '5', '5');

1、事务1【1.1】开启了快照读后,查询结果是(1,1,1)和(5,5,5);

2、事务2把了id=0的记录中c的字段更新为5;

3、事务1【1.2】中使用了select…for update的当前读,此时返回的结果不再是(1,1,1)和(5,5,5),而是 (0,5,0),(1,1,1)和(5,5,5),中间多出了(0,5,0)这条记录。



这是一种特殊的场景:在同一个事务中,先使用了快照读, 再使用当前读,两次的结果可能出现不一致。

可知RR级别下先使用非锁定读,再使用锁定读时,可能会出现幻读问题,这在MySQL中是允许出现的。

总结

MVCC是RC和RR隔离级别下利用【快照读】提升了读写并发的整体性能,主要通过Undo Log、版本链和Read View访问规则来实现。

MySQL MVCC原理深入探索的更多相关文章

  1. MySQL MVCC原理

    1 MVCC基本原理 MVCC:多版本并发控制(MVCC,Multiversion Currency Control).一般情况下,事务性储存引擎不是只使用表锁,行加锁的处理数据,而是结合了MVCC机 ...

  2. mysql MVCC原理理解

    MVCC多版本控制: 指的是一种提高并发的技术.最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞.引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了Inno ...

  3. Mysql 中的MVCC原理,undo日志的依赖

    一. MVCC 原理了解   原文点击:MVCC原理浅析 读锁: 也叫共享锁.S锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的 ...

  4. 深度探索MySQL主从复制原理

    深度探索MySQL主从复制原理 一 .概要 MySQL Replication (MySQL 主从复制) 是什么? 为什么要主从复制以及它的实现原理是什么? 1.1 MySQL 主从复制概念 MySQ ...

  5. mysql事务原理及MVCC

    mysql事务原理及MVCC 事务是数据库最为重要的机制之一,凡是使用过数据库的人,都了解数据库的事务机制,也对ACID四个 基本特性如数家珍.但是聊起事务或者ACID的底层实现原理,往往言之不详,不 ...

  6. Innodb 实现高并发、redo/undo MVCC原理

    一.并发控制   因为并发情况下有可能出现不同线程对同一资源进行变动,所以必须要对并发进行控制以保证数据的同一与安全.   可以参考CPython解释器中的GIL全局解释器锁,所以说python中没有 ...

  7. (转)MySQL备份原理详解

    MySQL备份原理详解 原文:http://www.cnblogs.com/cchust/p/5452557.html 备份是数据安全的最后一道防线,对于任何数据丢失的场景,备份虽然不一定能恢复百分之 ...

  8. (4.13)mysql备份原理(转)

    关键词:mysqldump原理,--single-transaction,mysql备份原理 转自:https://www.cnblogs.com/cchust/p/5452557.html MySQ ...

  9. MySQL索引原理及慢查询优化

    原文:http://tech.meituan.com/mysql-index.html 一个慢查询引发的思考 select count(*) from task where status=2 and ...

随机推荐

  1. springmvc学习日志二

    一.当接受的参数为日期类型时 1.建立jsp页面,向Controller类传入参数 1.1当传入的参数为单个时 <body> <form action="user/toDa ...

  2. java 文件上传(图片上传)

    1.FTP工具类 代码如下: package com.taotao.common.utils; import java.io.File; import java.io.FileInputStream; ...

  3. Java程序设计学习笔记(一)

    时间:2015-6-2 23:04 程序员,程序猿,程序媛,码农 -------------------------------------------------------   --Java的应用 ...

  4. Go错误处理正确姿势

    1. panic 在什么情况下使用panic? 在程序启动的时候,如果有强依赖的服务出现故障时panic退出 在程序启动的时候,如果发现有配置明显不符合要求,可以panic退出(预防编程) 其他情况下 ...

  5. springboot静态资源路径制定

    spring.resources.static-location参数指定了Spring Boot-web项目中静态文件存放地址, 该参数默认设置为: classpath:/static, classp ...

  6. Android系统编程入门系列之应用内键值对数据的简单保存

    在应用程序间及与用户的通信交互过程中,会产生并传递一系列数据.针对这些数据,有部分是只在应用程序中使用的缓存数据,还有一部分是在不同位置多次或长时间使用的持久化数据. 对于缓存数据来说,通常以代码中定 ...

  7. 关于ES6中Promise的应用-顺序合并Promise,并将返回结果以数组的形式输出

    1.Promise 基础知识梳理 创建一个Promise实例 const promise = new Promise(function(resolve, reject) { if (success){ ...

  8. PyCharm——滚动鼠标调整字体大小

  9. SciPy笔记

    一.简介 SciPy 是一个开源的 Python 算法库和数学工具包.Scipy 是基于 Numpy 的科学计算库,用于数学.科学.工程学等领域,很多有一些高阶抽象和物理模型需要使用 Scipy.Sc ...

  10. jq的选择器中带有特殊符号无法获取元素

    因项目需要,将元素id命名为数组(array[i].string) 使用jq去获取该id的元素时,返回的是个undefined.即jq获取不到该元素,因为该元素中的id含有特殊字符"[&qu ...