什么是MVCC?看看它的实现原理

大家好,我是归思君~

之前在讲 MySQL 事务隔离性提到过,对于写操作给读操作的影响这种情形下发生的脏读、不可重复读、虚读问题,是通过MVCC 机制来进行解决的,那么MVCC到底是如何实现的,其内部原理是怎样的呢?我们要抓住三个方面:记录中的4个隐藏字段、undo log 和 read view。

一、MVCC 定义和解决的读问题

1. 事务并发一致性的读问题

脏读(Dirty Read)

脏读也就是当前事务读取到了其他事务还未提交的数据。我们举个例子来看看:

Time session A session B
1 -设置当前会话事务隔离级别为:读未提交 set session transaction isolation level read uncommitted;
2 -设置当前会话事务隔离级别为:读未提交 set session transaction isolation level read uncommitted;
3 start transaction; select * from account;
4 start transaction; select * from account; update account set user_name = '孙七' where id = 6;
5 select * from account; 查询到了session B 中还没有提交的数据

不可重复读(Non-Repeatable Read)

不可重复读是两次读取的结果不相同,和脏读的区别就是不可重复读读到了其他事务提交后的数据。

举个实例来看看:

Time session A session B
1 -设置当前会话事务隔离级别为:读已提交 set session transaction isolation level read committed;
2 -设置当前会话事务隔离级别为:读已提交 set session transaction isolation level read committed;
3 start transaction; select * from account;
4 start transaction; select * from account; update account set user_name='赵赵' where id = 1; -此时已经发生修改 select * from account;
5 select * from account;
6 commit;
7 select * from account;对于未提交的事务,查询不到。相对于前一个隔离级别,杜绝了未提交事务修改对另外会话的影响。一旦另外的会话提交后,在进行查询时,会查出相应的修改。即在一个完整会话中,前后查询不同。

虚读(Phantom)

所谓虚读,也就是根据某些搜索条件先后查询数据库,发现两次查询结果条数不同。和不可重复读的区别就是不可重复读的条数没有变化,虚读条数因为修改操作造成了条数变化。

下面举个实例来说明:

Time session A session B
1 -设置当前会话事务隔离级别为:可重复读 set session transaction isolation level repeatable read; select @@transaction_isolation;
2 -设置当前会话事务隔离级别为:可重复读 set session transaction isolation level repeatable read; select @@transaction_isolation;
3 start transaction; select * from account;
4 start transaction; select * from account; insert into account values(7,'刘八',100); -此时已经发生修改 select * from account;
5 select * from account;
6 commit;
7 select * from account; insert into account values(7,'刘八',100);虽然此时查询全表没有发现新的数据,但是这个时候插入和session B 中相同的插入语句却提示存在一条 key = 7 的语句,说明 session B 的操作确实影响到了 session A 。 这就是虚读

2.MVCC的定义

全称叫 Multi-Version Concurrency Control 的多版本并发控制。也就是指"维持一个数据的多个版本,使得读写操作没有冲突"。

在说明 MVCC 原理前,先了解一下 InnoDB 的当前读和快照读:

当前读

当前读,也就是它读取的是记录的最新版本,而且还要保证其他并发事务不能修改当前记录,实现方式是对读取记录进行加锁。比如下面给出的都是当前读

sql 复制代码
#共享锁
select lock in share mode;
select for update;
#排他锁
update
insert
delete

快照读

快照读是一种基于多版本并发控制(MVCC)的不加锁读取形式,由于多版本控制,使得快照读读到的可能不是数据的最新版本。比如不加锁的select 操作就是快照读。

二、MVCC 实现原理

1. 记录的三个隐藏字段

对于InnoDB 存储引擎来说,它的每条聚簇索引记录中都包含有以下三个隐藏字段:

  • row_id:隐藏主键。如果该数据表中没有设置主键,就会自动生成一个6字节的row_id
  • roll_pointer:回滚指针。 指向旧版本的 undo 日志
  • trx_id:最近修改记录的事务ID。记录创建这条记录或者最后一次修改该记录的事务ID

如图所示,row_id 表示该记录生成的唯一隐式主键;trx_id 表示当前操作该记录的事务ID;roll ptr 是指向上一版本的 undo 日志的地址。

2. undo 日志

undo log 就是回滚日志,之前在事务的原子性中介绍过,它是保证事务原子性的机制。undo 日志保存的只有 insertdeleteupdate这些修改记录的操作。下面举个例子来帮助理解 undo log 的执行流程:

  • 1.有一个事务编号为1 的事务向数据表中插入一条记录,此时事务的状态是:

    • row_id:隐藏主键为1
    • trx_id:创建该记录的事务ID
    • roll ptr:其上个版本的 undo 日志为空
  • 2.第二个事务编号为2的事务对该记录进行修改,将name 字段的 ethan 改为 bob。此时的操作有:

    • 修改数据时,数据库会对该行加排他锁
    • 把该行数据拷贝一份到 undo log 中
    • 拷贝完成后,再修改该记录name 字段的 ethan 为 bob、修改隐藏字段的事务ID 为2,回滚指针指向拷贝到 undo log 的记录。
    • 事务提交后释放排他锁
  • 3.若第三个事务ID 为 3 对记录的age 字段进行了修改,将 20 修改为 18,则会出现:

    • 事务3修改记录时,数据库对该行加排他锁
    • 数据库将该行数据拷贝到 undo log 中
    • 拷贝完毕后将该记录字段的 age 改成 18。修改隐藏事务ID 为 3,回滚指针指向上个版本的地址
    • 事务提交后释放锁

从第二次我们会发现,undo log 中会出现多个版本的日志。这就是版本链。链首是最新的旧记录,链尾是最早的旧记录。

3. ReadView(读视图)

ReadView 定义

ReadView 是事务进行快照读那一刻,生成的一个数据系统当前的快照,记录并维护当前活跃事务的id,并且这个 ID 值是递增的。ReadView 的作用就是用来做可见性判断,记录当前事务执行快照读时,创建的ReadView 能够看到哪些版本的数据。

那么是ReadView 是怎么判断的呢?

ReadView 版本可见性判断规则

在ReadView 视图中主要有四个重要的属性:

  • trx_list: 一个数值列表,当前系统活跃的读写事务的事务id 列表
  • min_trx_id: trx_list 中最小的事务id,trx_list 中的最小值
  • max_trx_id: 不是trx_list 的最大值,它是指系统应该分配给下一事务的事务id
    • 比如现在 trx_list 中有id 为1、2、3、4的事务,那么max_trx_id 的值就是5
  • creator_trx_id:生成该 ReadView 事务的事务ID

在访问某条记录时,只需要按照下面的步骤来判断记录的某个版本是否可见:

  • 1.(trx_id == creator_trx_id)若被访问版本的trx_id值与当前 ReadView 中的 creator_trx_id 相同,也就是说当前事务在访问它自己修改过的记录,该版本可以被当前事务访问。
  • 2.(trx_id < min_trx_id)若被访问版本的trx_id 值小于 ReadView 的 min_trx_id 值,表明生成该版本的事务在当前事务生成ReadView 以前已经提交,该版本可以被当前事务访问。
  • 3.(trx_id >=max_trx_id)若被访问版本的trx_id 值大于或等于 ReadView 中的 max_trx_id ,表明生成该版本的事务在当前事务生成 ReadView 后才开启,该版本可以被当前事务访问。
  • 4.(min_trx_id <trx_id < max_trx_id)若被访问版本的trx_id 值介于 ReadView 的 min_trx_idmax_trx_id 值之间,需要判断trx_id 属性值是否存在 trx_list
    • 如果存在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问
    • 如果不存在,说明创建 ReadView 时生成该版本的事务已经被提交,因此该版本可以被访问

如果某个版本的数据对当前事务是不可见的,那就顺着版本链找到下一个版本数据,继续执行上面的步骤来判断记录的可见性,依次类推。知道版本中的最后一个版本。如果记录的最后一个版本也不可见,意味着该条记录对当前事务完全不可见,查询结果就不包含该记录。

举例

下面让我们来看看 MVCC 实现的具体流程是怎样的,如下表是事务ID 为2 的事务对某行数据执行了快照读,其中的列表如下:

事务1 事务2 事务3 事务4
事务开始 事务开始 事务开始 事务开始
... ... ... 修改且已提交
进行中 快照读 进行中
... ... ...

那么此时ReadView 的参数值为:

  • trx_list:事务1、2、3
  • min_trx_id:事务1
  • max_trx_id:事务5
  • creator_trx_id:事务2

以事务4 版本为例,我们经过上述规则来比较看当前ReadView 能否看见事务4版本的数据:

  • 经比较,只有第四条规则满足。此时trx_id 的值是介于min_trx_idmax_trx_id 之间,但是不在 trx_list 中,因此经判断该事务已经提交。所以该版本可以被访问。

其实这个规则很好理解,在活跃事务列表里面的,意味还没有提交,除了创建ReadView 的当前事务,其他的事务都不可见。不在列表里面的说明都已经提交,自然可以看见。如下图除了黄色和红色不可见,其他的版本都可见。

三、MVCC 如何解决脏读、不可重复读和虚读

首先回顾一下MySQL的事务隔离级别中的视图

  • 读未提交(RU):它是直接返回记录的最新值,没有视图
  • 读已提交(RC):每次查询都会创建一个ReadView
  • 可重复读(RR):这个ReadView是在事务启动时创建,整个事务存在期间都用这个ReadView
  • 串行化(serializable):直接用加锁的方式来避免并行访问

1.MVCC 解决脏读

在读已提交的MVCC 中,每次查询都会创建一个 ReadView 。由于版本控制的可见性规则,使得当前事务只看的到已经提交的数据,所以这样就避免了看见未提交的数据,从而解决了脏读。

2.MVCC 解决不可重复读

因为RC 级别每次查询都会创建一个 ReadView ,所以对于已提交的事务,由于不能共用一个ReadView ,还是会造成两次读取过程中的不可重复读。所以RR 级别通过使用从启动到结束使用一个 ReadView, 来解决提交两次查询读取不一致的现象。

3.MVCC 到底能不能解决虚读?

先说结论:MVCC可以解决"快照读",无法解决"当前读"

MVCC 可以解决"快照读"

MVCC 可以解决如不加锁的select。原理就是MVCC 使用快照来控制版本数据读取的范围,从而在 RR 级别避免了虚读。在我上面讲虚读的举例就说明了,在select 快照读时,没有发现新的数据。但是新插入同样的数据却报错,说明MVCC 无法彻底解决虚读。

MVCC 无法解决"当前读"

如果在select 上加锁,使用"当前读",虚读还是会出现。所以真正要解决虚读,还是得用加锁的形式来解决。所以一般而言,也只有串行化级别才能真正解决虚读。

参考资料

www.cnblogs.com/kismetv/p/1...

pdai.tech/md/db/sql-m...

time.geekbang.org/column/arti...

blog.csdn.net/qq_35590091...

《MySQL是怎样运行的-从根儿上理解MySQL》

本文参与了1024 程序员节活动,欢迎正在阅读的你也加入。

相关推荐
Fleshy数模8 分钟前
CentOS7 安装配置 MySQL5.7 完整教程(本地虚拟机学习版)
linux·mysql·centos
az44yao1 小时前
mysql 创建事件 每天17点执行一个存储过程
mysql
一点程序2 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
秦老师Q2 小时前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
橘子133 小时前
MySQL用户管理(十三)
数据库·mysql
Dxy12393102163 小时前
MySQL如何加唯一索引
android·数据库·mysql
我真的是大笨蛋3 小时前
深度解析InnoDB如何保障Buffer与磁盘数据一致性
java·数据库·sql·mysql·性能优化
怣503 小时前
MySQL数据检索入门:从零开始学SELECT查询
数据库·mysql
怪兽源码4 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
人道领域4 小时前
javaWeb从入门到进阶(SpringBoot事务管理及AOP)
java·数据库·mysql