Mysql - 从一个小 case 理解 MVCC

原文链接: https://juejin.cn/post/7163934829984088095

从 innoDB 的一致性非锁定读说起

非锁定读和行快照数据

一致性的非锁定读(consistent nonlocking read)是指 InnoDB 存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行 DELETE 或 UPDATE 操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB 存储引擎会去读取行的一个快照数据,下图是关于快照数据的一个简单示图:

之所以称其为非锁定读,因为不需要等待访问的行上 X 锁的释放。快照数据是指该行的之前版本的数据,该实现是通过 undo 段来完成。而 undo 用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。因此,非锁定读机制可以极大地提高数据库的并发性。

在 InnoDB 存储引擎的默认设置下,非锁定读是默认的读取方式,即读取不会占用和等待表上的锁。

  • 在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读
  • 即使都是使用非锁定的一致性读,对于快照数据的定义也各不相同

MVCC 的定义

快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。

一些前提知识

在开始案例分析之前,这里先简单介绍一些准备知识,如数据库级别的查看和设置,数据库事务的简单使用命令等。

数据库隔离级别的设置和查看

  • 1、查看数据库隔离级别
1
select @@global.tx_isolation,@@tx_isolation;
  • 2、设置数据库隔离级别
1
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

设置数据库隔离级别 scope 有两种,一种是当前会话级别,另一种是全局级别,示例如下:

1
2
3
4
// 全局的
mysql> set global transaction isolation level read uncommitted;
// 当前会话
mysql> set session transaction isolation level read uncommitted;

数据库事务的基本使用

  • 1、通过 SET AUTOCOMMIT=0 禁止自动提交
  • 2、用 BEGIN, ROLLBACK,COMMIT 来完成事务的基本操作
    • BEGIN 开始事务
    • ROLLBACK 回滚事务
    • COMMIT 提交事务

事务隔离级别的案例分析

前面简单介绍了非锁定读、MVCC 以及和本案例相关的一些数据库基本操作知识,下面来介绍在不同在不同事务隔离级别下,事务之间对于数据可见性和隔离性的一些基本问题。

案例说明

本案例中,提供了一张 orders 表,包括 id 和 marks 两个字段,id 为主键

1
2
3
4
5
6
7
mysql> select * from orders;
+----+-----------+
| id | marks |
+----+-----------+
| 1 | test1 |
| 2 | test2 |
+----+-----------+

在案例中将通过开启不同的事务级别来进行测试。大致思路是:

  • 1、禁止事务自动提交
  • 2、将全局事务的隔离级别和会话级别的隔离级别都设置成一样的
  • 3、开启两个会话窗口,在两个会话窗口内分别开启两个事务,在事务 A 中更新 id =x 的 mark 记录
    • 未提交时,分别在事务 A 和 事务 B 中查询 id =x 的记录;
    • 提交后,分别在事务 A 和 事务 B 中查询 id =x 的记录;

其中 1和 2 用于保证条件一致,3 为需要测试的操作。

read uncomitted

1、将两个会话的事务级别设置成相同的,均为 read uncommitted

2、在事务 A 中执行更新 orders 表中 id 为 1 的记录

3、分别在两个事务中查询 id=1 的记录

现象:在事务 A 没有提交的情况下,事务 B 可以看到事务 A 中更新的记录值了,这就是脏读。此时回滚事务 A,然后再次查询:

所以对于 read uncommitted,不同事务之间数据都可见,没有隔离性可言。

read comitted

将事务级别设置成 read committed,然后在事务 A 中更新 id 为 1 的记录。

  • 未 commit 时,事务 A 中更新的值对于事务 B 是不可见的;这也解释了事务 B 读取的快照数据。

  • commit 之后,事务 B 中可以读取到事务 A 更新的值了。

READ COMMITTED 事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。

repeatable read

将事务级别设置成 repeatable read,然后在事务 A 中更新 id 为 1 的记录;

  • 事务 A commit 之前,事务 A 中更新的数据对于事务 B 是不可见的;说明事务 B 读取的快照数据。

  • 事务 A commit 之后,事务 B 读取的还是以前的值,并没有读取到事务 A 中更新的值。结束事务 B 之后,再次查询,事务 B 查询到的值才是最新的。

REPEATABLE READ 事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

关于幻读

笔者在没有进行这个测试之前,对于幻读的意义理解是停留在类似这种描述上的:

当某个事务在读取某个范围的记录的时候,另外一个事务又在该范围插入了新的记录,当前事务再次读取这个范围的记录,会产生幻行(Phantom Data)– 《高性能MySQL》第三版

首先这个描述没有问题,笔者之前的理解是:在一个事务中 连续两次查询结果不一致(前提是基于可重复读隔隔离级别下),那这句话的反意就是,在一个事务中,如果连续两次查询结果一致,就不是幻读。

来看下面的案例。

场景 1

使用 range 查询,IS 锁场景,事务 B 中插入主键间隙之内的一条数据。

在可重复读的隔离级别下,事务 A 满足之前提到的可重复读的情况,不满足前面 在一个事务中 连续两次查询结果不一致 的说法;那么这里对于幻读的解释实际上就是:

事务A 没有正常读取到最新的事务,理论上应该有 3 条数据,而实际查询出来只有 2 条,这种情况对于事务 A 来说产生了幻读?

场景 2

在事务 A 和事务 B 中插入同一条数据。这种情况,因为在两个事务中同时写入一条数据,当事务 A 写入 id 为 6 数据,但是没有提交事务的时候,理论上事务 B 又写入 id 为 6 的数据会被阻塞住,那么对于事务 B 来说,它就需要知道事务 A 中有同样的操作;来看案例

此时事务 B 被阻塞等待事务 A 的提交。

当 事务 A 提交之后,事务 B 抛出异常。再次在事务 B 中查询,理论上如果存在幻读情况,事务 B 中将读取不到 id 为 6 的记录值,经测试事务 B 中读取到了 事务 A 中 提交之后的最新数据,因此对于这种情况,事务 B 在事务开始时查询到的结果没有 6,随后又执行了一次同样的查询操作,但是返回的结果确包含了 id 为 6 的记录,因此产生幻读。

这里相比于 case 1 ,事务 B 在查询第二次之前做了一次 insert 操作,insert 有一个潜在的规则是在插入数据之前需要读取当前最新记录数据,这也就和读提交读取最新记录是一致的,而不是读取的事务开始之前的数据了。

关于幻读的总结

ANSI SQL 隔离级别标准里可重复读级别是存在幻读问题;但是 InnoDB 的可重复读级别 通过MVCC机制解决了幻读问题!所以 InnoDB 的可重复读是不存在幻读问题的(这里的幻读指的是:当某个事务在读取某个范围的记录的时候,另外一个事务又在该范围插入了新的记录,当前事务再次读取这个范围的记录,会产生幻行(Phantom Data))。

case 2 中由于触发了当前读而导致数据冲突的问题,才导致了“幻读”的情况。insert、update 等语句执行之前,会先 select,再执行 insert、update。简单说,就是先读一次,再执行更新语句。而且这个读,是读最新的数据!

关于脏读、不可重复读、幻读

上述案例中,

  • 在 read uncommitted 隔离级别下,事务 B 可以读取到事务 A 未提交的数据,这种情况称之为 脏读。
  • 在 read committed 隔离界级别下,事务 B 可以读取到事务 A 已经提交的数据,但是在当前事务 B 处理过程之内,意味着其它事务的数据变更都会影响到事务 B 中获取到的行数据的值,这种情况称之为 不可重复读
  • 在 repeatable-read 隔离级别下,分为两种情况:
    • 事务 A 中仅执行两次 range 查询,事务 B 插入新数据并提交事务时,事务 A 中第二次查询不会产线幻读情况。
    • 事务 A 执行两次插入查询中间执行 insert 操作,且于事务 B 中存在锁冲突时,事务 A 会将快照读改为当前读,从而第一次查询和第二次查询结果不一致。。

参考

https://www.zhihu.com/question/47007926

作者

卫恒

发布于

2022-11-12

更新于

2023-04-20

许可协议

评论