MVCC

重点知识

MVCC 指行多版本控制。提高数据库并发,也是事务隔离的实现机制。

使用 MVCC 可以做到在对同一行数据进行读写操作的时候不用加锁。这个读指的是 快照读。

MVCC 是一个概念,这个概念的实现就是 快照读。
快照读操作只读取该事务开始前的数据库快照。

按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。

事务启动时有一个 事务 id,叫做 transaction id。是在事务开始时候向 InnoDB 申请的,严格按照申请顺序递增的。

每次事务更新数据的时候都会把 transaction id 赋值给这个数据版本的事务 id,记为 row trx_id。
一行数据可能有多个版本,每个版本都有一个 row trx_id。

在实现上, InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的就是,启动了但还没提交。

例子

mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

insert into t(id, k) values(1,1);

事务A、B、C的执行流程

问:MySQL InnoDB 默认隔离级别下,事务 A、B、C 里,K 的值分别是多少?

尝试回答下

事务 C 没有指定手动提交事务,所以是自动提交的,并且 C 事务先执行的,所以 K 是 2。
事务 B 在执行,由于 B 执行了 update,会以当前读的方式获取最新数据,所以 C 提交后的事务,B 是可以看到的,所以,K 是 3。

事务 A 最后执行,由于是 RR 级别,可重复读,并且是当前读,所以与事务开始的时候保持一致,所以 K 是 1 。

详细分析

这里,我们不妨做如下假设:

  1. 事务A开始前,系统里面只有一个活跃事务 ID 是99;
  2. 事务 A、B、C的版本号分别是100、101、102,且当前系统里只有这四个事务;

这样,事务A的视图数组就是[99,100], 事务B的视图数组是[99,100,101], 事务C的视图数组是[99,100,101,102]。

如何得到事务 A 的结果

简化下,只画出跟事务A查询逻辑有关的操作:
事务A查询数据逻辑图

一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

  1. 版本未提交,不可见;
  2. 版本已提交,但是是在视图创建后提交的,不可见;
  3. 版本已提交,而且是在视图创建前提交的,可见。

现在,我们用这个规则来判断图中的查询结果,事务A的查询语句的视图数组是在事务A启动的时候生成的,这时候:

  1. (1,3)还没提交,属于情况1,不可见;
  2. (1,2)虽然提交了,但是是在视图数组创建之后提交的,属于情况2,不可见;
  3. (1,1)是在视图数组创建之前提交的,可见。

如何得到事务 B 的结果

一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”
因此,在更新的时候,当前读拿到的数据是(1,2),更新后生成了新版本的数据(1,3),这个新版本的row trx_id是101。

所以,在执行事务B查询语句的时候,一看自己的版本号是101,最新数据的版本号也是101,是自己的更新,可以直接使用,所以查询得到的k的值是3。

事务的可重复读的能力是怎么实现的

可重复读的核心就是一致性读(consistent read);
而事务更新数据的时候,只能用当前读。
如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

  1. 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
  2. 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

这里需要说明一下,“start transaction with consistent snapshot; ”的意思是从这个语句开始,创建一个持续整个事务的一致性快照。所以,在读提交隔离级别下,这个用法就没意义了,等效于普通的start transaction。


undo 链
read view 视图

-- 《MySQL 实战 45 讲》,03、08讲。
https://juejin.cn/post/6871046354018238472#heading-3

2021/8/17 posted in  MySQL