文章目录

  • MVCC 产生背景
  • InnoDB 引擎表的隐藏列
  • Undo 回滚版本链
  • 一致性视图
  • MVCC 实现原理
  • 举例说明 MVCC 实现过程


MVCC 产生背景

最早的数据库系统,只有读读之间可以并发,读写,写读,写写之间都要阻塞。而 MVCC (Muti Version Concurrency Control) , 是一种多版本并发控制机制。在引入 MVCC 之后,只有写写之间相互阻塞,其他的三种操作都可以并行,这样大幅度提高了 InnoDB 的并发量。

InnoDB 引擎表的隐藏列

使用 InnoDB 引擎的表的每行记录有 2 个隐藏列:

  • trx_id : 行事务 ID, 记录的是造成该次数据变更(增/删/改)的事务 ID;
  • roll_pointer : 回滚指针, 指向上一个版本的数据记录;

这两列就是实现 MVCC 机制的关键所在。

Undo 回滚版本链

使用 InnoDB 引擎的表的每行记录,都会维护着一条 Undo 回滚版本链,例如有这样一张表:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '名称',
  `age` smallint(6) NOT NULL COMMENT '年龄',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

现在表中插入了这样一条数据,且造成这次新增的事务 ID35

mysql 有没有 nvarchar mysql nvcc_数据

再假设我们现在把这条数据改成了 3 次:

  • 第一次:将 name 改成王霸之气,造成这次修改的事务 ID73
  • 第二次:将 age 改成 21,造成这次修改的事务 ID81
  • 第三次:将 name 改成王老二,将 age 改成 16,造成这次修改的事务 ID85

那么这条数据在 Innodb 中实际的结构大概如下所示:

mysql 有没有 nvarchar mysql nvcc_mysql_02

如图所示,由一条数据的历史版本数据组成的链式结构即为 Undo 回滚版本链

一致性视图

当一个 MySQL 客户端开启事务,并执行第一次查询 SQL 语句时,将会生成专属于这个会话的一致性视图read-view)。这个一致性视图组成部分是:

  • 当前所有未提交的事务 ID 数组,其中数组里最小的 ID 记为 min_id
  • 当前已经创建的最大事务 ID(注意 Innodb 中的事务 ID 是连续且递增的),记为 max_id

注意,当隔离级别是 RR(可重复读)时,这个视图在该事务结束前都不会发生变化;但当隔离级别是 RC(读已提交)时,每执行一次查询语句,就会重新生成一次一致性视图。

MVCC 实现原理

有了 Undo 回滚版本链一致性视图MVCC 就能顺利实现了。先说结论:

MVCC 机制的实现就是通过 Undo 回滚版本链和一致性视图两者的对比机制,使得不同事务会根据记录版本链对比规则读取同一条数据在版本链上的不同版本数据。

举例说明 MVCC 实现过程

RR 隔离级别为例,我们还是用那条数据,现在假设这条数据的回滚版本链变成了下面这个样子(注意事务提交情况这一列是虚构的):

mysql 有没有 nvarchar mysql nvcc_mysql 有没有 nvarchar_03

假设现在有一个事务查询了这条数据(select name from user where id = 1),又假设当前 Innodb 内最大的事务 ID 是 110,那么它查到的会是哪个结果呢?这个查询过程大概如下所示:

  • 首先生成一致性视图
  • 收集当前系统中所有未提交的事务 ID(假设当前有 3 个未提交的事务,事务 ID 分别为 97、100、105),并获取数组最小的事务 ID,记为 min_id = 97
  • 记录当前系统中已生成的最大事务 ID,记为 max_id = 110
  • 从 Undo 回滚版本链的第一条数据开始,逐条进行分析
  • 首先判断该 trx_id 是否大于 max_id(如果是,那么代表这是一条未来生成的事务,是不可见的)
  • 如果 trx_id 不大于 max_id,继续判断 trx_id 是否小于 min_id(如果是,那么代表这是一条已经提交的事务,是可见的)
  • 如果 trx_id 不小于 min_id,继续判断 trx_id 是否在未提交的事务 ID 数组里面
    - 如果 trx_id 在未提交的事务 ID 数组里面,则这条数据还没有提交,是不可见的
    - 如果 trx_id 不在未提交的事务 ID 数组里面,则这条数据已经提交,是可见的
  • 找到第一条可见的数据后即返回该条数据,否则返回空

根据上面的流程,我们很容易就知道:

  • 地址为 0x9e80f(name = 王一)这条数据,trx_id 不大于 max_id,也不小于 min_id,它的 trx_id 又恰好在未提交的事务 ID 数组中,所以这条数据是不可见的
  • 地址为 0x0a0f1(name = 王老二)这条数据,trx_id 不大于 max_id,但是小于 min_id,所以这条数据是可见的

所以在这个场景下,select name from user where id = 1 这条 SQL 语句的查询结果应该返回的是 王老二
继续延申一下,如果 trx_id = 105 这个事务已经提交了,那么这个查询事务查询到的结果是什么呢?很显然,因为一致性视图没有变化,所以 trx_id = 105 这个变化还是不可见的,所以查询结果还是 王老二