MySQL MVCC

1、简介

MVCC(Multi-Version Concurrency Control,多版本并发控制),是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。用于支持 读已提交(RC) 和 可重复读(RR) 隔离级别的实现。

MVCC的实现依赖于六个概念:【隐式字段】【undo日志】【版本链】【快照读和当前读】【读视图】。

2、InnoDB 表的隐藏字段

在 MySQL 中,InnoDB 会为每行记录后面添加两个 or 三个隐藏字段:

  • DB_ROW_ID(可能有):行ID,MySQL 的 B+Tree 索引特性要求每个表必须要有一个主键。如果没有设置的话,会自动寻找第一个不包含 NULL 的唯一索引列作为主键。如果还是找不到,就会在这个 DB_ROW_ID 上自动生成一个唯一值,以此来当作主键(该列和MVCC的关系不大)。
  • DB_TRX_ID:事务ID,占用 6byte 的标识,记录的是当前事务在做 INSERT 或 UPDATE 语句操作时的 事务ID(DELETE语句被当做是UPDATE语句的特殊情况)。
  • DB_ROLL_PTR:回滚指针,占用 7byte,指向这条记录的上一个版本的 undo log 记录,通过它可以将不同的版本串联起来,形成版本链。相当于链表的 next指针。

注意,添加的隐藏字段并不是很多人认为的创建时间和删除时间,同时在 MySQL 中 MVCC 的实现也不是通过什么快照来实现的。之所以有这种说法可能是源自于《高性能MySQL》一书中对 MySQL 中 MVCC 的错误结论,然后就人云亦云传开了(注意,这里一直强调的是 MySQL 中 MVCC 的实现,是因为在不同的数据库中可能会有不同的实现)。所以说看源码和看官方文档才是最权威的解释。

#、3 undo log

undo log 一种用于撤销回退的日志。

undo log记录的是什么? undo log 中记录的是当前事务操作中的相反操作。如:

  • 一条 insert 语句在 undo log 中会对应一条 delete 语句。
  • 一条 delete 语句在 undo log 中会对应一条 insert 语句。
  • 一条 update 语句在 undo log 中对应相反的 update 语句。

Undo log的工作原理

  1. 执行 update 操作时,事务A 提交时候(事务还没提交),会将数据进行备份,备份到对应的 undo buffer,当事务回滚时或者数据库崩溃时用于回滚事务。

undo log 的主要作用是事务回滚和实现 MVCC 快照读。

undo log 分为两种:

  • insert undo log:事务在 insert 新记录时产生的 undo log。仅用于事务回滚,并且在事务提交后可以被立即丢弃。
  • update undo log:事务在 update 或 delete 时产生的 undo log。不仅在事务回滚时需要,在实现 MVCC 快照读时也需要,所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被清理线程统一清除。

MVCC 实际上是使用的 update undo log 实现的快照读。

InnoDB 并不会真正地去开辟空间存储多个版本的行记录,只是借助 undo log 记录每次写操作的反向操作。所以 B+Tree 上对应的记录行只会有一个最新的版本,但是 InnoDB 可以根据 undo log 得到数据的历史版本,从而实现多版本控制。

4、版本链

当事务对某一行数据进行改动时,会产生一条 undo log,多个事务同时操作一条记录时,就会产生多个版本的 undo log,这些日志通过回滚指针(DB_ROLL_PTR)连成一个链表,称为版本链。

假设现在有一张 account 表,其中有 id 和 name 两个字段,那么版本链的示意图如下:

5、快照读和当前读

快照读(Consistent Read,也叫普通读)

快照读读取的是记录数据的可见版本,不加锁,不加锁的普通 select 语句都是快照读,即不加锁的非阻塞读。

快照读的执行方式是生成 ReadView,直接利用 MVCC 机制来进行读取,并不会对记录进行加锁。

如下语句:

1
select * from table;

当前读(Locking Read,也称锁定读)

当前读 读取的是记录数据的最新版本,并且需要先获取对应记录的锁。如下语句:

1
2
3
4
5
SELECT * FROM student LOCK IN SHARE MODE;  # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁

6、读视图(Read View)

Read View 提供了某一时刻事务系统的快照,主要是用来做可见性判断, 里面保存了【对本事务不可见的其他活跃事务】。

开启事务后,会产生一个 Read View,实际是在执行 select 语句前才生成当前事务的 Read View,用来判断当前事务可见哪个版本的数据,即可见性判断。

Read View 的四个属性

MySQL5.7 源码中对 Read View 定义了四个属性,如下:

  • creator_trx_id: 创建当前 Read View 的事务ID。
  • m_ids: 当前系统中所有的活跃事务(当前系统中开启了事务,但还没有提交的事务)的 id。
  • m_low_limit_id: 当前系统中活跃的读写事务中最小的事务id,即 m_ids 中的最小值。
  • m_up_limit_id: 当前系统中事务的 id 值最大的那个事务 id 值再加 1,也就是系统中下一个要生成的事务 id。

Read View 会根据这 4 个属性,结合 undo log 版本链中的属性,来实现 MVCC 机制,从而决定一个事务最后能读取到数据的哪个版本。

假设现在有 事务A 和 事务B 并发执行,事务A 的事务 id 为 10,事务B 的事务 id 为 20。

  • 事务A 的 Read View:m_ids=[10,20],m_low_limit_id=10,m_up_limit_id=21,creator_trx_id=10

  • 事务B 的 Read View:m_ids=[10,20],m_low_limit_id=10,m_up_limit_id=21,creator_trx_id=20

当执行 SELECT 语句的时候会创建 Read View,但是在读取已提交和可重复读两个事务级别下,生成 Read View 的策略是不一样的:

  • 读取已提交级别是每执行一次 SELECT 语句就会重新生成一份 Read View。
  • 而可重复读级别是只会在第一次 SELECT 语句执行的时候会生成一份,后续的 SELECT 语句会沿用之前生成的 Read View(即使后面有更新语句的话,也会继续沿用)。

Read View 可见性判断规则

当一个事务读取某条数据时,会通过每一条记录的隐藏字段 DB_TRX_ID 在坐标轴上的位置来进行可见性规则判断,如下:

  • DB_TRX_ID < m_low_limit_id: 表示 DB_TRX_ID 对应这条数据 在当前事务开启 creator_trx_id 之前,其他的事务就已经将该条数据修改了并提交了事务(事务的 id 值是递增的),所以当前事务能读取到。

  • DB_TRX_ID >= m_up_limit_id: 表示在当前事务 creator_trx_id 开启以后,有新的事务开启,并且新的事务修改了这行数据的值并提交了事务,因为这是 creator_trx_id 后面的事务修改提交的数据,所以当前事务 creator_trx_id 是不能读取到的。

  • m_low_limit_id =< DB_TRX_ID < m_up_limit_id:

    • DB_TRX_ID 在 m_ids 数组中: 表示 DB_TRX_ID 和当前事务 creator_trx_id 是在同一时刻开启的事务。

      • DB_TRX_ID 不等于 creator_trx_id: DB_TRX_ID 事务修改了数据的值,并提交了事务,而这个事务不是自己,所以当前事务 creator_trx_id 不能读取到。

      • DB_TRX_ID 等于 creator_trx_id: 表明数据是自己修改并提交的,因此是可见的。

    • DB_TRX_ID 不在 m_ids 数组中: 表示的是在当前事务 creator_trx_id 开启之前,其他事务 DB_TRX_ID 将数据修改后就已经提交了事务,所以当前事务能读取到。

7、MVCC实现原理

通过上述对 Read View 的分析可以总结出:InnoDB 实现 MVCC 是通过 Read View 与 Undo Log 实现的,Undo Log 保存了历史快照,形成版版本链,Read View 可见性规则判断当前版本的数据是否可见。

InnnoDB 执行查询语句的具体步骤为:

  1. 执行语句之前获取查询事务自己的 事务ID,即事务版本号。

  2. 通过 事务ID 获取 Read View。

  3. 查询存储的数据,将其 DB_TRX_ID 与 Read View 中的事务版本号 creator_trx_id 进行比较。

  4. 不符合 Read View 的可见性规则,则读取 Undo log 中历史快照数据。

  5. 找到当前事务能够读取的数据返回。

而在实际的使用过程中,Read View 在不同的隔离级别下是得工作方式是不一样。

  • 读已提交(Read committed, RC)MVCC 实现原理:在读已提交的隔离级别下实现 MVCC,同一个事务里面,每一次查询都会产生一个新的 Read View 副本,这样可能造成同一个事务里前后读取数据可能不一致的问题(不可重复读并发问题)。

  • 可重复读(Repeatable read,RR)MVCC 实现原理:在可重复读的隔离级别下实现 MVCC,同一个事务里面,多次查询,都只会产生一个共用 Read View,都是使用的执行第一次 select 语句时生成的 Read View,以此不可重复读并发问题。

Reference


MySQL MVCC
https://flepeng.github.io/041-MySQL-41-底层原理-MySQL-MVCC/
作者
Lepeng
发布于
2020年8月8日
许可协议