数据库 MVCC 详解

目录

[1. 什么是 MVCC?](#1. 什么是 MVCC?)

[2. MVCC 的好处?](#2. MVCC 的好处?)

[3. 快照读?当前读分别是什么?怎么理解?](#3. 快照读?当前读分别是什么?怎么理解?)

[3.1 快照读](#3.1 快照读)

[3.2 当前读](#3.2 当前读)

[4. 数据库的四种隔离级别](#4. 数据库的四种隔离级别)

[5. MVCC 实现原理](#5. MVCC 实现原理)

[5.1 隐藏字段](#5.1 隐藏字段)

[5.2 undo log(版本链)](#5.2 undo log(版本链))

[5.3 readView](#5.3 readView)

[6. readView 深层详解](#6. readView 深层详解)

[7. MVCC中是如何解决不可重复读的?](#7. MVCC中是如何解决不可重复读的?)

[8. 间隙锁解决幻读问题(补充点)](#8. 间隙锁解决幻读问题(补充点))


1. 什么是 MVCC?

MVCC 英文全称叫 "Multiversion Concurrency Control",翻译过来就是 "多版本并发控制"。在 MySQL 众多存储引擎中只有 InnoDB 中实现了 MVCC 机制。

2. MVCC 的好处?

首先我们要清楚,在 InnoDB 存储引擎下,假设事务A我们对一行数据进行修改操作,是会对这一行数据进行加写锁的;如果此时事务B来查询这一行数据,它就要加读锁,读锁与写锁冲突,所以十五B、加锁不成功,它就必须等待事务A操作执行完毕释放写锁之后才能去进行读操作。

而有了 MVCC 的加入,我们的事务B再去查询该行数据时,就不需要等待事务A释放锁可以直接查询,查询方式是快照读(下面会解释到),查询到的是事务A修改数据之前当前行的数据,提高了数据库的并发效率。

总之一句话:MVCC 是通过数据行的多版本管理来实现数据库的并发控制,提高数据库的并发性能。

3. 快照读?当前读分别是什么?怎么理解?

3.1 快照读

我们姑且把刚才的事务A的查询操作理解为写操作,事务B的查询操作理解为读操作;在 MVCC 下,这里的读指的是快照读。了解在 Linux 操作系统和 Git 代码管理的大致应该清楚,我们可以通过 Linux 操作系统的快照将系统回溯到之前的某个版本,Git 也可以通过回溯版本返回至之前的某个代码版本。MVCC 中的快照与这两者大致意思相近,可以类比理解。

数据在修改之前和修改之后版本是不一样的,我们读取别人正在操作的数据时,可以读取该数据操作之前的快照,就可以避免读写锁互斥导致阻塞等待这一现象。

3.2 当前读

当前读就很好理解了,没有 MVCC 时,数据库是靠加锁来避免数据安全性问题,加的锁都是悲观锁。共享锁,排它锁都属于是当前读的一种范畴。

我们去读取数据,读取到的一定是当前数据,没有数据版本这一说法。假设要去读一个正在被修改的数据,是会阻塞的,只有别人修改完,才能去执行当前读这一操作,也可以理解为同步读。

4. 数据库的四种隔离级别

数据库有四种隔离级别。它们的隔离级别由低到高,并发能力由高到低。

没有 MVCC 的情况下

读未提交:解决了脏写问题;

读已提交:解决了脏写,脏读问题;

可重复读:解决了脏写,脏读,不可重复读;

串行化: 解决了脏写,脏读,不可重复读,幻读所有问题;

在有 MVCC 的情况下

可重复读:解决了脏写,脏读,不可重复读,幻读所有并发问题。数据库默认采用的也是可重复读,解决幻读正是因为采用了 MVCC 。

读已提交和可重复读的读数据方式采用的都是快照读的方式。读未提交则不可以,因为读未提交独到的就是最新的数据,无法使用快照;串行化也不可以,因为加锁的缘故,也无法使用快照。

5. MVCC 实现原理

MVCC 实现原理主要依赖于三部分,隐藏字段,undo log版本链,readView。

5.1 隐藏字段

对于 InnoDB 存储引擎的表来说,它的聚簇索引记录(理解为每行数据即可)中都会有两个必要的隐藏字段 trx_id(事务id) 和 roll_pointer(回滚指针) 。没有主键的表会有第三个额外的隐藏主键字段。隐藏字段的主要作用就是对每次数据操作进行标记区分并记录操作之前的数据的地址。

我就以下面这幅图来给各位解析一下 trx_id 和 roll_pointer.

trx_id:每次一个事务对聚簇索引的记录做改动,都会把该事务的事务id赋值给隐藏字段。

roll_pointer:每次对聚簇索引的记录做改动时,都会把旧的版本写入到 undo 日志中去,然后这个隐藏列相当于一个指针,可以通过它来找到该记录修改之前的数据。

如上图,假设与四个事务A,B,C,D。事务A插入数据,事务B,C,D均对插入的数据做了修改。四个事务在对数据进行增删改查的时候,数据库就会给这四个事务的隐藏字段 trx_id 以自增的方式赋值,这里 假设分别赋值为 1,2,3,4。

roll_pointer 回滚指针则是指向当前数据修改之前的数据值,倘若事务回滚,就会返回到之前的数据。

5.2 undo log(版本链)

如上所示四个事务进行的四次数据更新操作,每次数据操作之后,数据库都会把操作之前的旧值存放到 undo 日志中记录下来,随着更新次数的增多,每次记录都会由隐藏字段中的 roll_pointer 指针连接起来形成链表,所形成的链表我们就称之为版本链,链表的头节点就是当前数据最新的节点。

5.3 readView

刚才我们说到了版本链,既然一条数据经历了多次操作,有那么多个版本,我们在查询数据并进行操作的时候,是怎么知道该选择哪个版本的数据的呢?一定是查询操作最新的吗?这是不一定的。查询操作哪个版本的数据取决于我们的第三个重要元素 readView。

readView 就是事务在使用 MVCC 机制对数据库中的数据操作时产生的读视图。当事务开启之后,会生成数据库当前系统的一个快照,InnoDB 会为每个事务构建一个数组,用来记录并维护当前系统活跃事务的id (这里的活跃指代的是事务正在操作数据但是没有进行提交)。

6. readView 深层详解

readView 是MVCC 三个中最重要的组成部分,也是面试 MVCC 时经常问道的一个点。

readView 的核心原理主要体现在 READ COMMITTD(读已提交)和 REPEATABLE READ(可重复读) 两种隔离级别上。

READ COMMITTD:在每次进行 SELECT 查询操作的时候都会去生成一个 readView;

REPEATABLE READ:每开启一个事物才会生成一个 readView,一个事务的所有SQL语句共享一个 readView。

readView 有多个属性,m_ids 就可以理解为生成的数组记录,如下图所示,基于以下几种属性, 一共有四种可能情况。

情况一 trx_id == creator_trx_id:说明这条记录就是当前事务插入所形成的,自己插入的数据自己肯定可以访问;

情况二 trx_id < min_trx_id: min_trx_id表示的是正在活跃的事务最小的 id,而所有活跃的事物都是未提交的,所以就可以查询得到,不会出现读未提交的情况;

情况三 trx_id > max_trx_id: max_trx_id表示要分配给下一个事务的 id,二我们要查询的数据的 id 却比待分配的事物的 id 还要大,这是不可能查得到的。

情况四 min_trx_id <= trx_id <= max_trx_id:如果 trx_id 是在m_ids 中,则不可以访问这个版本,因为在此区间内则说明此当前事务正在进行中还没提交,不能访问其他事务未提交的数据,否则可能会产生脏读。如果不在m_ids 中,说明当前事务已经是 commit 提交过了的,则可以访问。

7. MVCC中是如何解决不可重复读的?

在 MVCC 中 可重复读的隔离级别下,它也解决了幻读。在 MVCC 下,它是给每一个事务生成一个 readView,整个事务的执行过程中用的都是同一个 readView。

举个最简单的例子,如下所示

(1)假设现在事务A与事务B并发操作来查询 student 表,事务A 执行查询操作,执行查询操作之前会生成一个 readView,我们姑且称之为 readView_1 ,事务A从始至终使用的都是 readView_1;

(2)此时事务B来修改 student 数据,又生成了一个 readView ,我们称之为 readView_2,然后事务B率先修改完毕并提交;

(3)事务A在事务B提交之后才进行的查询,按道理来说因为事务B修改了数据,我们会产生不可重复读,但是因为事务A从始至终都是用的 readView_1 ,所以 事务A在进行查询操作的时候,查询到的其实还是事务B修改之前的数据,由此就解决了不可重复读。

8. 间隙锁解决幻读问题(补充点)

刚才我已经解释过了 MVCC 中是如何解决不可重复读问题的,在 InnoDB 存储引擎中,幻读的问题也得到了解决,解决的方式是利用间隙锁;

还以下面这幅图举例说明

假设事务A与事务B并发执行,事务A要查询 id > 1的用户数据,那么在查询之前,数据库会对 id = 1 之后的区间加上间隙锁,也就是说在事务A执行期间,其他线程不可以在 id > 1 之后插入数据;当有其他操作想要插入数据时,会阻塞等待,只有事务A执行完毕释放了间隙锁,其他线程或者说事务才能进行插入操作,由此就避免了幻读的产生。

相关推荐
qq_2131578911 分钟前
(c#)unity中sqlite多线程同时开启事务会导致非常慢
数据库·sqlite·c#
北极无雪17 分钟前
Spring源码学习(拓展篇):SpringMVC中的异常处理
java·开发语言·数据库·学习·spring·servlet
666xiaoniuzi39 分钟前
深入理解 C 语言中的内存操作函数:memcpy、memmove、memset 和 memcmp
android·c语言·数据库
正在走向自律1 小时前
3.使用条件语句编写存储过程(3/10)
数据库·存储过程·安全架构
YONG823_API1 小时前
电商平台数据批量获取自动抓取的实现方法分享(API)
java·大数据·开发语言·数据库·爬虫·网络爬虫
小小不董1 小时前
图文深入理解Oracle DB Scheduler
linux·运维·服务器·数据库·oracle
大拇指的约定2 小时前
数据库(MySQL):使用命令从零开始在Navicat创建一个数据库及其数据表(三),单表查询
数据库·mysql·oracle
阳光阿盖尔2 小时前
redis——哨兵机制
数据库·redis·缓存·主从复制·哨兵
小小娥子2 小时前
【Redis】Hash类型的常用命令
数据库·spring boot·redis
盒马盒马2 小时前
Redis:cpp.redis++通用接口
数据库·c++·redis