MySQL InnoDB之MVCC原理是什么

58次阅读
没有评论

共计 8728 个字符,预计需要花费 22 分钟才能阅读完成。

今天丸趣 TV 小编给大家分享一下 MySQL InnoDB 之 MVCC 原理是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,主要是为了提高数据库的并发性能。同一行数据平时发生读写请求时,会上锁阻塞住。但 MVCC 用更好的方式去处理读—写请求,做到在发生读—写请求冲突时不用加锁。这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。那它到底是怎么做到读—写不用加锁的,快照读和当前读是指什么?我们后面都会学到。

MySQL 在 REPEATABLE READ 隔离级别下,是可以很大程度避免幻读问题的发生的,MySQL 是怎么做到的?

版本链

我们知道,对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id 并不是必要的,我们创建的表中有主键或者非 NULL 的 UNIQUE 键时都不会包含 row_id 列):

trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务 id 赋值给 trx_id 隐藏列。

roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

为了说明这个问题,我们创建一个演示表:

CREATE TABLE `teacher` ( `number` int(11) NOT NULL,
 `name` varchar(100) DEFAULT NULL,
 `domain` varchar(100) DEFAULT NULL,
 PRIMARY KEY (`number`)) ENGINE=InnoDB DEFAULT CHARSET=utf8

然后向这个表里插入一条数据:

mysql  insert into teacher values(1,  J ,  Java Query OK, 1 row affected (0.01 sec)

现在里的数据就是这样的:

mysql  select * from teacher;
+--------+------+--------+
| number | name | domain |
+--------+------+--------+
| 1 | J | Java |
+--------+------+--------+
1 row in set (0.00 sec)

假设插入该记录的事务 id 为 60,那么此刻该条记录的示意图如下所示:

假设之后两个事务 id 分别为 80、120 的事务对这条记录进行 UPDATE 操作,操作流程如下:

Trx80Trx120begin

beginupdate teacher set name=‘S’where number=1;
update teacher set name=‘T’where number=1;
commit

update teacher set name=‘K’where number=1;
update teacher set name=‘F’where number=1;
commit

每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,所以现在的情况就像下图一样:

对该记录每次更新后,都会将旧值放到一条 undo 日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务 id。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为多版本并发控制 (Mulit-Version Concurrency Control MVCC)。

ReadView

对于使用 READ UNCOMMITTED 隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。

对于使用 SERIALIZABLE 隔离级别的事务来说,InnoDB 使用加锁的方式来访问记录。

对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:READ COMMITTED 和 REPEATABLE READ 隔离级别在不可重复读和幻读上的区别,这两种隔离级别关键是需要判断一下版本链中的哪个版本是当前事务可见的。

为此,InnoDB 提出了一个 ReadView 的概念,这个 ReadView 中主要包含 4 个比较重要的内容:

m_ids:表示在生成 ReadView 时当前系统中活跃的读写事务的事务 id 列表。

min_trx_id:表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务 id,也就是 m_ids 中的最小值。

max_trx_id:表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。注意 max_trx_id 并不是 m_ids 中的最大值,事务 id 是递增分配的。比方说现在有 id 为 1,2,3 这三个事务,之后 id 为 3 的事务提交了。那么一个新的读事务在生成 ReadView 时,m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4。

creator_trx_id:表示生成该 ReadView 的事务的事务 id。

有了这个 ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。

如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。

如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间 (min_trx_id = trx_id max_trx_id),那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,事务还没提交,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。

我们还是以表 teacher 为例,假设现在表 teacher 中只有一条由事务 id 为 60 的事务插入的一条记录,接下来看一下 READ COMMITTED 和 REPEATABLE READ 所谓的生成 ReadView 的时机不同到底不同在哪里。

READ COMMITTED 每次读取数据前都生成一个 ReadView

假设现在系统里有两个事务 id 分别为 80、120 的事务在执行:

# Transaction 80
set session transaction isolation level read committed;
begin
update teacher set name= S  where number=1;
update teacher set name= T  where number=1;

此刻,表 teacher 中 number 为 1 的记录得到的版本链表如下所示:

假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:

set session transaction isolation level read committed;
#  使用 READ COMMITTED 隔离级别的事务
begin;
# SELECE1:Transaction 80、120 未提交
SELECT * FROM teacher WHERE number = 1; #  得到的列 name 的值为 J

这个 SELECE1 的执行过程如下:

在执行 SELECT 语句时会先生成一个 ReadView,ReadView 的 m_ids 列表的内容就是 [80, 120],min_trx_id 为 80,max_trx_id 为 121,creator_trx_id 为 0。

然后从版本链中挑选可见的记录,最新版本的列 name 的内容是’T’,该版本的 trx_id 值为 80,在 m_ids 列表内,根据步骤 4 不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是’S’,该版本的 trx_id 值也为 80,也在 m_ids 列表内,根据步骤 4 也不符合要求,继续跳到下一个版本。

下一个版本的列 name 的内容是’J’,该版本的 trx_id 值为 60,小于 ReadView 中的 min_trx_id 值,根据步骤 2 判断这个版本是符合要求的。

之后,我们把事务 id 为 80 的事务提交一下,然后再到事务 id 为 120 的事务中更新一下表 teacher 中 number 为 1 的记录:

set session transaction isolation level read committed;
# Transaction 120
begin
update teacher set name= K  where number=1;
update teacher set name= F  where number=1;

此刻,表 teacher 中 number 为 1 的记录的版本链就长这样:

然后再到刚才使用 READ COMMITTED 隔离级别的事务中继续查找这个 number 为 1 的记录,如下:

#  使用 READ COMMITTED 隔离级别的事务
begin;
# SELECE1:Transaction 80、120 未提交
SELECT * FROM teacher WHERE number = 1; #  得到的列 name 的值为 J 
# SELECE2:Transaction 80 提交、120 未提交
SELECT * FROM teacher WHERE number = 1; #  得到的列 name 的值为 T

这个 SELECE2 的执行过程如下:

在执行 SELECT 语句时会又会单独生成一个 ReadView,该 ReadView 的 m_ids 列表的内容就是 [120](事务 id 为 80 的那个事务已经提交了,所以再次生成快照时就没有它了),min_trx_id 为 120,max_trx_id 为 121,creator_trx_id 为 0。

然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是’F’,该版本的 trx_id 值为 120,在 m_ids 列表内,根据步骤 4 不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是’K’,该版本的 trx_id 值为 120,也在 m_ids 列表内,根据步骤 4 不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是’T’,该版本的 trx_id 值为 80,小于 ReadView 中的 min_trx_id 值 120,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为’‘T’的记录。

以此类推,如果之后事务 id 为 120 的记录也提交了,再次在使用 READCOMMITTED 隔离级别的事务中查询表 teacher 中 number 值为 1 的记录时,得到的结果就是’F’了,具体流程我们就不分析了。

总结一下就是:使用 READCOMMITTED 隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView。

REPEATABLE READ —— 在第一次读取数据时生成一个 ReadView

对于使用 REPEATABLE READ 隔离级别的事务来说,只会在第一次执行查询语句时生成一个 ReadView,之后的查询就不会重复生成了。我们还是用例子看一下是什么效果。

假设现在系统里有两个事务 id 分别为 80、120 的事务在执行:

# Transaction 80
begin
update teacher set name= S  where number=1;
update teacher set name= T  where number=1;

此刻,表 teacher 中 number 为 1 的记录得到的版本链表如下所示:

假设现在有一个使用 REPEATABLE READ 隔离级别的事务开始执行:

#  使用 REPEATABLE READ 隔离级别的事务
begin;
# SELECE1:Transaction 80、120 未提交
SELECT * FROM teacher WHERE number = 1; #  得到的列 name 的值为 J

这个 SELECE1 的执行过程如下(与 READ COMMITTED 的过程一致):

在执行 SELECT 语句时会先生成一个 ReadView,ReadView 的 m_ids 列表的内容就是 [80, 120],min_trx_id 为 80,max_trx_id 为 121,creator_trx_id 为 0。

然后从版本链中挑选可见的记录,最新版本的列 name 的内容是’T’,该版本的 trx_id 值为 80,在 m_ids 列表内,根据步骤 4 不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是’S’,该版本的 trx_id 值也为 80,也在 m_ids 列表内,根据步骤 4 也不符合要求,继续跳到下一个版本。

下一个版本的列 name 的内容是’J’,该版本的 trx_id 值为 60,小于 ReadView 中的 min_trx_id 值,根据步骤 2 判断这个版本是符合要求的。

之后,我们把事务 id 为 80 的事务提交一下,然后再到事务 id 为 120 的事务中更新一下表 teacher 中 number 为 1 的记录:

# Transaction 80
begin
update teacher set name= K  where number=1;
update teacher set name= F  where number=1;

此刻,表 teacher 中 number 为 1 的记录的版本链就长这样:

然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个 number 为 1 的记录,如下:

#  使用 REPEATABLE READ 隔离级别的事务
begin;
# SELECE1:Transaction 80、120 未提交
SELECT * FROM teacher WHERE number = 1; #  得到的列 name 的值为 J
# SELECE2:Transaction 80 提交、120 未提交
SELECT * FROM teacher WHERE number = 1; #  得到的列 name 的值为 J 

这个 SELECE2 的执行过程如下:

因为当前事务的隔离级别为 REPEATABLE READ,而之前在执行 SELECE1 时已经生成过 ReadView 了,所以此时直接复用之前的 ReadView,之前的 ReadView 的 m_ids 列表的内容就是 [80, 120],min_trx_id 为 80,max_trx_id 为 121,creator_trx_id 为 0。

然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是’F’,该版本的 trx_id 值为 120,在 m_ids 列表内,根据步骤 4 不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是’K’,该版本的 trx_id 值为 120,也在 m_ids 列表内,根据步骤 4 不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是’T’,该版本的 trx_id 值为 80,也在 m_ids 列表内,根据步骤 4 不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是’S’,该版本的 trx_id 值为 80,也在 m_ids 列表内,根据步骤 4 不符合可见性要求,根据 roll_pointer 跳到下一个版本。

下一个版本的列 name 的内容是’J’,该版本的 trx_id 值为 60,小于 ReadView 中的 min_trx_id 值 80,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为’‘J’的记录。

也就是说两次 SELECT 查询得到的结果是重复的,记录的列值都是’’‘J’’’,这就是可重复读的含义。

如果我们之后再把事务 id 为 120 的记录提交了,然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个 number 为 1 的记录,得到的结果还是’J’,具体执行过程大家可以自己分析一下。

MVCC 下的幻读现象和幻读解决

前面我们已经知道了,REPEATABLE READ 隔离级别下 MVCC 可以解决不可重复读问题,那么幻读呢?MVCC 是怎么解决的?幻读是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,而这个记录来自另一个事务添加的新记录。

我们可以想想,在 REPEATABLE READ 隔离级别下的事务 T1 先根据某个搜索条件读取到多条记录,然后事务 T2 插入一条符合相应搜索条件的记录并提交,然后事务 T1 再根据相同搜索条件执行查询。结果会是什么?按照 ReadView 中的比较规则:

不管事务 T2 比事务 T1 是否先开启,事务 T1 都是看不到 T2 的提交的。请自行按照上面介绍的版本链、ReadView 以及判断可见性的规则来分析一下。

但是,在 REPEATABLE READ 隔离级别下 InnoDB 中的 MVCC 可以很大程度地避免幻读现象,而不是完全禁止幻读。怎么回事呢?我们来看下面的情况:

T1T2begin;
select * from teacher where number=30; 无数据 begin;
insert into teacher values(30,‘X’,‘Java’);
commit;update teacher set domain=‘MQ’where number=30;
select * from teacher where number = 30; 有数据

嗯,怎么回事?事务 T1 很明显出现了幻读现象。在 REPEATABLE READ 隔离级别下,T1 第一次执行普通的 SELECT 语句时生成了一个 ReadView,之后 T2 向 teacher 表中新插入一条记录并提交。ReadView 并不能阻止 T1 执行 UPDATE 或者 DELETE 语句来改动这个新插入的记录(由于 T2 已经提交,因此改动该记录并不会造成阻塞 ),但是这样一来,这条新记录的 trx_id 隐藏列的值就变成了 T1 的事务 id。之后 T1 再使用普通的 SELECT 语句去查询这条记录时就可以看到这条记录了,也就可以把这条记录返回给客户端。因为这个特殊现象的存在,我们也可以认为 MVCC 并不能完全禁止幻读。

MVCC 小结

从上边的描述中我们可以看出来,所谓的 MVCC(Multi-Version ConcurrencyControl,多版本并发控制)指的就是在使用 READ COMMITTD、REPEATABLE READ 这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程,这样子可以使不同事务的读 - 写、写 - 读操作并发执行,从而提升系统性能。

READ COMMITTD、REPEATABLE READ 这两个隔离级别的一个很大不同就是:生成 ReadView 的时机不同,READ COMMITTD 在每一次进行普通 SELECT 操作前都会生成一个 ReadView,而 REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 就好了,从而基本上可以避免幻读现象。

我们之前说执行 DELETE 语句或者更新主键的 UPDATE 语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的 delete mark 操作,相当于只是对记录打上了一个删除标志位,这主要就是为 MVCC 服务的。另外,所谓的 MVCC 只是在我们进行普通的 SEELCT 查询时才生效,截止到目前我们所见的所有 SELECT 语句都算是普通的查询,至于什么是个不普通的查询,后面就会讲到。

以上就是“MySQL InnoDB 之 MVCC 原理是什么”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,丸趣 TV 小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注丸趣 TV 行业资讯频道。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-13发表,共计8728字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)