5

Mysql - 从一个小 case 理解 MVCC

 1 year ago
source link: https://blog.51cto.com/u_15773567/5840769
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

非锁定读和行快照数据

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

Mysql - 从一个小 case 理解 MVCC_数据

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

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

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

MVCC 的定义

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

一些前提知识

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

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

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

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

// 全局的
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 为主键

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

Mysql - 从一个小 case 理解 MVCC_数据库_02

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

Mysql - 从一个小 case 理解 MVCC_隔离级别_03

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

Mysql - 从一个小 case 理解 MVCC_数据_04

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

Mysql - 从一个小 case 理解 MVCC_隔离级别_05

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

read comitted

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

  • 未 commit 时,事务 A 中更新的值对于事务 B 是不可见的;这也解释了事务 B 读取的快照数据。
Mysql - 从一个小 case 理解 MVCC_数据_06
  • commit 之后,事务 B 中可以读取到事务 A 更新的值了。
Mysql - 从一个小 case 理解 MVCC_数据库_07

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

repeatable read

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

  • 事务 A commit 之前,事务 A 中更新的数据对于事务 B 是不可见的;说明事务 B 读取的快照数据。
Mysql - 从一个小 case 理解 MVCC_隔离级别_08
  • 事务 A commit 之后,事务 B 读取的还是以前的值,并没有读取到事务 A 中更新的值。结束事务 B 之后,再次查询,事务 B 查询到的值才是最新的。
Mysql - 从一个小 case 理解 MVCC_数据库_09

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

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

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

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


来看下面的案例。

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

Mysql - 从一个小 case 理解 MVCC_隔离级别_10

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

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

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

Mysql - 从一个小 case 理解 MVCC_数据_11

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

Mysql - 从一个小 case 理解 MVCC_隔离级别_12

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

Mysql - 从一个小 case 理解 MVCC_数据库_13

这里相比于 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 会将快照读改为当前读,从而第一次查询和第二次查询结果不一致。。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK