你好,我是亚风,今天我来跟你聊聊 MySQL 的 MVCC 机制。

随着去 IOE 的推进,以及各大互联网公司对 MySQL 的广泛使用,MySQL 俨然已经成为 数据库技术选型的老大,而对 MySQL 数据库而言,MVCC 机制是极其核心的一环。如果 没有 MVCC 机制,MySQL 将无法保证在高并发下数据一致性和访问性能之间的平衡。 MVCC 是一个优雅的数据库并发访问解决方案,无论你是 MySQL DBA 还是应用开发 者,都应该熟练掌握。

今天,我就来带你看一看怎么更好地理解 MVCC。

要聊 MVCC,就无法不提及事务。所以,为了方便你理解,我会先带你学习一下 MySQL 事务,然后再看看 MVCC 在不同事务隔离级别下,分别是如何工作的。

对了,因为今天讨论的机制都是在 InnoDB 引擎下工作的,所以这一讲不会讨论 MySQL 的其他存储引擎。

MySQL 事务原理

​你应该知道,事务具备四种特性:原子性、一致性、隔离性和持久性,这四种特性一般可 以简称为 ACID。在事务中的操作,要么全部执行,要么全部回滚。事务的实现核心是基于两个文件,也就是重做日志 redo log 和回滚日志 undo log。

不过,在这里我要提醒你一句,其实在 MVCC 中并不会使用重做日志 redo log,但是, 为了让你能够系统性地学习相关知识,我会把 MySQL 事务原理的两种日志全部给你分析一遍。

redo log

首先聊聊 redo log,它可以保证事务的持久性,即事务 ACID 中的 D。绝大多数情况下 redo log 记录的是页的物理变化。页是数据库的 IO 单元,可以简单理解为一个固定大小DML 对页的修改操作,都会产生 redo log。在理解的时候,你可以把 redo log 的内容 想象成 DML 语句即可。

redo log 记录了一系列 DML 操作,因此可以用来进行数据恢复。redo log 分为两部分,一部分在内存中也就是 redo log buffer,一部分在磁盘文件中,也叫 redo log file。和现有的主流日志框架一样,日志先写入内存,再异步持久化到磁盘。

下面我来带你看看 redo log 的完整流程:

第一步,先将旧的数据从磁盘中读入内存;

第二步,生成一条 redo log 并写入 redo log buffer;

第三步,当事务 commit 时,将 redo log buffer 中的内容持久化到磁盘文件;

第四步,定期将内存中修改的数据写到磁盘。

第一步和第二步比较基础,主要是把旧数据以及对旧数据的变更放到内存,在此不再赘 述。现在,重点来看上面的第三步,当事务 commit 时,先将 redo log buffer 写入到 redo log file 进行持久化,这种做法被称为 Write Ahead Log,也就是日志先行,这是 什么意思呢?就是说,在持久化一个数据页之前,先将内存中相应的日志持久化。

​那为什么必须要先写日志呢?可不可以不写日志,直接将数据写入磁盘?原则上是可以 的,不过会产生一些问题,数据修改会产生随机 IO,但日志是顺序 IO,它采用追加方式 顺序写,也就是串行写入,这样才能充分利用磁盘的性能。当一个事务提交时,其产生所 有的日志必须写到磁盘中,若在日志写入磁盘后,内存中的数据持久化前数据库发生了宕 机,那么数据库重启时,可以通过日志来保证数据的完整性。

undo log

下面来看 undo log,相比于 redo log,undo log 就简单多了,undo log 主要记录的是 数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然 后在发生错误时才可以回滚。

undo log 有两个主要作用,分别是回滚和 MVCC,我先来带你学习一下回滚。

undo log 只是将数据库逻辑恢复到原来的样子,在回滚的时候,它实际上却是在做相反 的工作,比如一条 INSERT,对应一条 DELETE,对于每个 UPDATE,对应一条相反的 UPDATE,将修改前的行放回去。undo 日志用于事务的回滚操作,保障了事务的原子 性。

log 的实现则显得有些粗糙。

敲黑板,总结一下重点:

  1. redo log 是物理日志,但逻辑上可以等同为 DML 语句;undo log 是逻辑日志,实 现比较简单本身就类似 DML。
  2. 事务采用日志先行来保证数据是持久的,不管三七二十一,先写日志再说。
  3. undo log 通过记录相反的操作实现回滚,还可以实现 MVCC。

MySQL 事务隔离级别

除了 redo log 和 undo log 以外,另一个你在学习 MVCC 之前不得不了解的知识,就是隔离级别。隔离级别是相当基础的知识,所以这里我们快速复习下就好。

隔离是为了解决以下三个问题而诞生的:

  1. 脏读:事务 A 读取了事务 B 没提交的数据。
  2. 不可重复读:事务 A 多次读取同一数据,事务 B 在这个过程中不断修改数据并提交, 导致事务 A 多次读取同一数据时,且每次都不一样。
  3. 幻读:范围查询的时候,每次返回的符合条件的结果行数在变化。

以下是各事物隔离级别,我们简单分析下:

首先是 read-uncommitted,这是最不严谨的隔离级别,这种情况下脏读、不可重复读、幻读都可能发生。read-committed 从名字上看只能读取已经提交的数据,显然解决了脏 读的问题。repeatable-read 则更进一步,解决了可重复读的问题。

你可能注意到了,repeatable-read 级别还可以处理幻读,这是 MySQL 独有的 next-key lock 实现的。而 serializable 则最为严格,同时效率也最低。

不过,你需要知道的是,MVCC 只在 read-committed 和 repeatable-read 两个隔离级 别下工作。其他两个隔离级别和 MVCC 不兼容,因为 read-uncommitted 总是读取最新 的数据行,而不读取符合当前事务版本的数据行。而 serializable 则会对所有读取的行都加锁。简单来说就是 serializable 和 read-uncommitted 太极端,用不上 MVCC。

所以,要学习 MVCC,你必须要关注 read-committed 和 repeatable-read 两种级别。 知道了这些,现在就进入正题,来看 MVCC 的相关知识。

MVCC 机制

首先,请你思考一个问题,为什么需要 MVCC 呢?没错,是因为数据库通常使用锁来实 现隔离性。原生的锁,锁住一个资源后会禁止其他任何线程访问同一资源。但是很多应用这个思想和 Java 中的 ReadWriteLock 非常类似。读锁和读锁之间不互斥,而写锁和写 锁、读锁都互斥。这样就提升了系统的并发能力。

之后人们发现并发读还是不够,又提出了能不能让读写之间也不冲突的方法,就是在读取 数据时通过副本的方式将数据保存下来,这样读锁就和写锁就不冲突了,这个思想很像 Java 中的 CopyOnWriteArrayList。当然副本是一种概念,不同的数据库、不同的版本, 可能会用不同方法去实现。其中 MySQL 的 MVCC 就是基于保存历史版本的数据来实现的。

上面你也可以看出为什么面试的时候,面试官总爱问你底层原理。因为复杂系统的设计总 是可以相互借鉴的。

MySQL 行记录中除了记录业务数据外,还有隐藏的 trx_id 和 roll_ptr,其中 trx_id 表示 最近修改的事务的 id,roll_ptr 指向该行上一个版本的地址,有点像链表的指针。新增一 个事务时,trx_id 会递增,因此 trx_id 能够表示事务开始的先后顺序。

举个例子,比如我向 user 表初始插入了下面一行数据:

此时发起一个事务,执行下面的语句:

 update user set name = '亚风 2' where id = 1;

trx_id 会自增到 2,新数据的 roll_ptr 指向了之前的记录,形成的一个单向链表,以后每 次更新都会追加到链表的后面,也就是 MySQL 的每行记录逻辑上其实是一个链表,是不 是很意外?是不是有点像早期的 HashMap?当然,这个链表存在于 undo log 中,和最 新版本的数据不在一起。

ReadView

说完了 undo log,再来说说 ReadView,你可能第一次听到这个单词,其实它很好理 解。MVCC 只在 read-committed 和 repeatable-read 两个隔离级别下工作,而 read-committed 和 repeatable-read 的区别就在于它们生成 ReadView 的策略不同。

继续用之前的例子来理解一下 ReadView 和 trx_ids,提交 trx_id 是 2 的记录后,接着有 一个 trx_id 为 3 的事务,修改 name 为亚风 3,但是事务还没提交。则此时的版本链是:

显然,此时的 trx_ids 为 3,如果另一个事务查询 id 为 1 的记录,因为 trx_ids 当前只有 事务 id 为 3 的事务,而 trx_ids 的意义是记录未完成的事务。在这里,事务未完成,所以 该条记录不可见,继续查询下一条,结果返回亚风 2。 这时我把 trx_id 为 3 的事务提交了,并且新建了一个 trx_id 为 4 也修改 id 为 1 的记录 name= 亚风 4,并且不提交事务。这时候版本链就是:

此时之前的事务又执行了一次查询,要查询 id 为 1 的记录。它将看到什么呢?

如果是 repeatable-read 隔离级别,将不会重建 ReadView,trx_ids 还是 3。所以 select 的结果是亚风 2。所以第 2 次 select 结果和第 1 次一样,所以叫可重复读。

也就是说 read-committed 隔离级别下的事务每次执行查询都会生成一个新的 ReadView,而 repeatable-read 隔离级别则在第一次查询时生成一个 ReadView,之后 的读都复用之前的 ReadView。

不过,在这里你要注意,两种隔离级别的差异是时间上的概念,其实二者是一样的,重新创建一个 ReadView 并没有改变整体流程,这是一个刷新的动作而已。

这就是 MySQL 的 MVCC 机制,通过 undo log 中的版本链表,实现多版本。通过 ReadView 生成策略的不同实现不同的隔离级别。

总结

今天我们一起学习了 MySQL 的 MVCC 机制。通过学习 MVCC,我们简单复习了 MySQL 的事务原理,了解了 undo log 和 redo log 的功能和区别,熟悉了四种隔离级别以及它们所解决的问题。另外,我们也学习了 MVCC 原理,深度挖掘 MVCC 机制,知道 了 read-committed 和 repeatable-read 的差异以及它们的实现原理。

MySQL 的 MVCC 除了是大厂的高频面试题之外,还有一定的普世价值,很多其他主流中 间件也经常会高效利用文件存储来实现事务、随机访问等特性。这也恰恰说明了,在计算 机的世界中,不同的技术栈可能有着相同的哲学。

最后希望我的分享可以帮到你,欢迎你在评论区给我留言,也欢迎把这篇文章分享给其他 人。

Last modification:August 19th, 2021 at 10:45 am
如果觉得我的文章对你有用,请随意赞赏