「MySQL高级篇」详解MVCC

930 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第七天,点击查看活动详情

大家好,我是Zhan,一名个人练习时长一年半的大二后台练习生,最近在学MySQL高级篇,欢迎各路大佬一起交流讨论

👉本篇速览

在前面对InnoDB存储引擎的的学习中,我们学习到了MySQL的逻辑存储结构InnoDB存储引擎的内存、磁盘结构以及后台线程、以及事务原子性、持久性、一致性的实现原理,不过我们埋了一个坑,事务隔离性的实现,这就是本文的重点,其实锁已经对事务的隔离性贡献了一部分,但是MVCC也是支持事务隔离性很重要的一部分,也是面试高频考点,本文将带着大家,从以下五个方面,深入了解MVCC

  • 1️⃣ MVCC-基本概念
  • 2️⃣ 数据库中的三个隐藏字段
  • 3️⃣ undolog是如何帮助实现MVCC的
  • 4️⃣ readView又是如何帮助实现MVCC的
  • 5️⃣ 综合上述三点对MVCC进行原理分析

Tips:本篇知识点比较多、比较密集、大家可以慢慢来看,相信认真啃完后会有不小的收获!


1️⃣ MVCC-基本概念

MVCC这里面涉及的知识点和概念比较多,并且相对复杂,因此,我们在讲解MVCC之前,有必要对MVCC的基本概念先做一个介绍,做完这些铺垫后,我们再来解析多版本并发控制(MVCC) 的底层原理:

🎐 当前读

当前读:我们读取的是记录的最新版本,读取时还要保证其他的并发事务不能修改当前记录,也就是对读取的记录进行加锁。

我们常见的当前读的操作有:SELECT……LOCK IN SHARE MODE、SELECT……FOR UPDATE、UPDATE、INSERT、DELETE

我们做个简单的示例:

左边的事务查询的过程中,右边的事务更新数据后,左边的查询的数据仍然是旧数据。右边的事务提交后,也就是数据库中的数据已经发生了改变,左边的数据仍然是没有变化

这与MySQL的事务隔离级别有关系,因为默认的隔离级别为可重复读(Repteatable Read),也就意味着SELECT语句不是当前读,那我们以 SELECT……LOCK IN SHARE MODE 作为当前读的示例,再次进入这个场景:

我们会发现当前读读到的数据是最新的数据,简单来说,当前读读取到的就是最新的数据记录


📢 快照读

快照读:就是简单的SELECT语句,也就是我们最开始演示的,它读取的是记录数据的可见版本,可能会是历史数据,它并不加锁,是非阻塞读

不同的事务隔离级别的快照读的实现不同:

  • Read Committed:每次SELECT,都会生成一个快照读
  • Repeatable Read:开启事务后,第一个SELECT语句才是快照读的地方,也就是说在第一个SELECT会产生一个快照,后续在查询的时候,就会去查询这个快照,也就是复用
  • Serializable:快照读会退化为当前读,每次操作都会去加锁

提到在RR隔离级别下,开启事务后第一个SELECT语句才产生快照,那如果在产生快照前进行了数据更新,是否产生的快照是更新后的呢,我们再次来到FinalShell做一个测试:

我们可以发现,尽管是在第一个SELECT语句才产生快照读,但是查到的数据仍然是插入前的数据


🔔 MVCC

MVCC:全称Multi-Version Concurrency Control,多版本并发控制。指的是维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读的功能。

MVCC的具体实现,依赖于数据库记录的三个隐式字段undo log日志readView,MVCC实现原理就取决于这三块内容,这就是本文的第二、三、四点,我们将针对这三点,做一个详细的讲解,从而了解MVCC底层的原理是怎么实现的


2️⃣ 隐式字段

我们在讲数据库的逻辑存储结构的时候,有提到,数据库的行,也就是数据是存在两个隐藏的字段的:BD_TRX_ID、DB_ROLL_PTR

但是我们在上面也提到了,是数据库记录的三个隐式字段,是因为还有一个DB_ROW_ID,这三个字段分别的含义如下:

隐藏字段含义
DB_TRX_ID最近修改事务ID,记录 插入这条记录或者最后一次修改该记录的事务ID
DB_ROLL_PTR回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本
DB_ROW_ID隐藏主键,如果表结构没有主键,将会生成该隐藏字段,也就是说表中有主键,就不会出现该隐式字段

那我们想要去确认是否存在隐式字段,以及隐式字段相关信息,就需要去查看表的结构,也就是我们在讲解存储引擎时提到的IDB文件,并使用命令:ibd2sdi xxx.ibd进行查看:

这是我们使用命令得到的信息,由于比较长,我们只截取我们需要的数据:DB_ROLL_PTR、DB_TRX_ID

这里没有DB_ROW_ID,因为存在主键,所以就不会出现该隐式字段


3️⃣ Undo Log & MVCC

Undo Log:回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志

在使用结束,即事务结束后,是可以被删除的,但是什么时候是立即删除,什么时候是不会立即删除呢,就分为以下两种

  1. 当insert的时候,产生的undo log日志只在回滚时需要,在事提交或者回滚后,可以被立即删除掉,
  2. 当update、delete的时候,产生的undo log日志不仅在回滚的时候需要,在快照读的时候也需要,不会立即删除

🔗 Undo Log 版本链

我们暂时不去想Undo Log 版本链是什么,先来看看这个场景:

先新插入一条数据,其各字段的值如下:

现有四个事务操作该记录,操作的方式和顺序如下,时间从上到下:

事务1事务2事务3事务4
开始事务开始事务开始事务开始事务
修改id为30的记录的age为3查询id为30的记录
提交事务
修改name为A3
查询id为30的记录
提交事务
修改age为10
查询id为30的记录
查询id为30的记录
提交事务

我们按照时间线来模拟一下事务的执行:

  1. 首先事务1修改年龄为3,在修改这条记录之前,需要记录undo log日志用于数据回滚,在记录结束后,会去进行更新操作: 这里为了可视化,记录的是物理日志,实际上应该是:

    UPDATE user SET age = 30 where id = 30 这条SQL语句

  2. 然后事务2开启事务,修改name为A3,此时图就变为了:

3. 依次类推事务3和事务4,就能得到:

所以这样就形成了undo Log版本链,不同的事务或者相同的事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的记录,链表的尾部是最早的旧记录。

当我们在进行查询的时候,应该返回哪一个版本呢,这并不由undolog版本链控制,而是MVCC实现原理的第三个组件:ReadView,我们在下文来探讨:


4️⃣ ReadView & MVCC

ReadView:读视图,是快照读SQL执行时MVCC提取数据的依据,记录并维护当前未提交的事务id。我们先回顾一下快照读:它读取的是记录数据的可见版本,可能会是历史数据。在刚才的讲解中我们提到:undolog版本链,在版本链中产生的记录都是历史记录,而快照读读取的究竟是哪个历史记录,就是由ReadView决定的。

那么ReadView是怎么实现的呢,我们先看看它的四个核心字段

字段含义
m_ids当前活跃(未提交的事务)的事务ID集合
min_trx_id最小活跃事务ID
max_trx_id预分配事务ID,当前最大事务ID+1(因为事务ID是自增的)
creator_trx_idReadView创建者的事务ID

有了上面的前置知识,我们来看看版本链数据访问规则,如果当前的事务ID为trx_id

  1. trx_id == creator_trx_id 代表可以访问该版本,说明这个更改是当前这个事务做的
  2. trx_id < min_trx_id 代表可以访问该版本,说明数据的更改已经提交了
  3. trx_id > max_trx_id 代表不可以访问该版本,说明这个事务是在ReadView生成后才开启的
  4. min_trx_id <= trx_id <= max_trx_id 如果 trx_id 不在 m_ids 中是可以访问该版本的,说明该数据已经提交

这个听起来有点不好理解,当然如果细心去揣摩是能够发现上述的访问规则是没有问题的,但是为了方便大家理解,我们将在下面的原理分析中,进行实际场景的使用。

在此之前,我们首先要了解到一点:不同的隔离级别,生成ReadView的时机不同:

  • READ COMMITTED:在事务每次执行快照读的时候生成 ReadView
  • REPEATABLE READ:仅仅 在事务第一次执行快照读的时候生成 ReadView

5️⃣ 原理分析

这里我们将带着上面的四个规则,进入实际场景,分别分析两种隔离级别下事务原理:

🛠 RC级别

还是之前的四个事务,但是我们只分析事务5的两个快照读:

生成快照读的时候的四个字段我们并不难理解。

比如第一个快照读,此时尚未提交的事务为3,4,5,因此m_ids:[3,4,5],其中最小的为3,最大的加一为6,因此min_trx_id = 3,max_trx_id = 6,创建这个快照读的事务为5,因此creator_trx_id = 5

第二个快照读依次类推也能得出,那么两个快照读的信息的我们都了解了,我们先从第一个快照读开始分析:

在之前讲undo log版本链的时候我们得到了这张图:

现在我们就要借用这张图和四个规则开始匹配,根据已知的数据,我们可以把规则中的数据填上方便我们进行匹配:

从版本链的最新的数据开始遍历:

  1. 首先事务ID为4,发现四条规则都匹配不上:4 != 5, 4 > 3, 4 < 6, 4虽然在3-6之间,但是存在于m_ids中
  2. 然后拿到版本链的下一个数据,事务ID为3:3 != 5, 3 == 3, 3 < 6, 3虽然在3-6之间,但是存在于m_ids中
  3. 然后拿到版本链的下一个数据,事务ID为2,匹配上了第二条规则,此时我们就确定该数据的版本是可以访问的,因此此处ReadView得到的数据就是版本链中事务Id为2的数据:

这么一看,好像我们今天提到的隐藏字段、undoLog版本链、ReadView就全部串起来了,首先是UndoLog版本链中的数据是需要隐藏字段的,然后ReadView需要遍历UndoLog版本链,找到合适的能够访问的数据。

我们此时来分析第二个快照读就应该熟练很多了:

  1. 首先把已知的数据代入进规则,方便后续进行比较:

  1. 然后遍历版本链,最后发现符合规则的数据为:事务ID为3的那条数据

⚒ RR级别

其实RR级别和RC级别的区别并不在于比较规则和比较流程,唯一的区别就在于ReadView的生成时机

  • RR 隔离级别下,仅仅在事务进行第一次快照读的时候生成ReadView,后续再复用该ReadView
  • RC 隔离级别下,每次在事务进行快照读的时候就会生成ReadView

也就是说,对于RR级别的ReadView为:

后续的比较我们就不再进行了,相信大家也能在脑海中想到了


💬 总结

本文讲解了MVCC的实现原理,MVCC的实现实际上是三个部分:隐藏字段UndoLog版本链ReadView

  1. 隐藏字段:主要取决于其中的两个隐藏字段:事务ID和回滚指针
  2. UndoLog版本链:这是我们在查询时选择版本,去遍历的链表,链表的头部是最新的数据
  3. ReadView:生成的ReadView以及它的四个字段,能够帮助我们正确的匹配到能够访问的版本的数据

本文我们讲了MVCC的底层实现原理,知识点比较密集,大家可以慢慢看

MVCC加上我们之前学到的三种粒度锁就能完成InnoDB存储引擎事务的隔离性,而undoLog、redoLog能够实现事务的原子性、一致性、持久性,至此,事务实现底层原理 the end!


🍁 友链


✒写在最后

都看到这里啦~,给个点赞再走呗~,也欢迎各位大佬指正,在评论区一起交流,共同进步!也欢迎加微信一起交流:Goldfish7710。咱们明天见~

求赞.jpeg