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

之所以称其为非锁定读,因为不需要等待访问的行上 X 锁的释放。快照数据是指该行的之前版本的数据,该实现是通过 undo 段来完成。而 undo 用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。因此,非锁定读机制可以极大地提高数据库的并发性。
在 InnoDB 存储引擎的默认设置下,非锁定读是默认的读取方式,即读取不会占用和等待表上的锁。
快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。
在开始案例分析之前,这里先简单介绍一些准备知识,如数据库级别的查看和设置,数据库事务的简单使用命令等。
select @@global.tx_isolation,@@tx_isolation;
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 来完成事务的基本操作
前面简单介绍了非锁定读、MVCC 以及和本案例相关的一些数据库基本操作知识,下面来介绍在不同在不同事务隔离级别下,事务之间对于数据可见性和隔离性的一些基本问题。
本案例中,提供了一张 orders 表,包括 id 和 marks 两个字段,id 为主键
mysql> select * from orders;
+----+-----------+
| id | marks |
+----+-----------+
| 1 | test1 |
| 2 | test2 |
+----+-----------+
复制代码
在案例中将通过开启不同的事务级别来进行测试。大致思路是:
1、禁止事务自动提交
2、将全局事务的隔离级别和会话级别的隔离级别都设置成一样的
3、开启两个会话窗口,在两个会话窗口内分别开启两个事务,在事务 A 中更新 id =x 的 mark 记录
其中 1和 2 用于保证条件一致,3 为需要测试的操作。
1、将两个会话的事务级别设置成相同的,均为 read uncommitted

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

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

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

所以对于 read uncommitted,不同事务之间数据都可见,没有隔离性可言。
将事务级别设置成 read committed,然后在事务 A 中更新 id 为 1 的记录。


READ COMMITTED 事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。
将事务级别设置成 repeatable read,然后在事务 A 中更新 id 为 1 的记录;


REPEATABLE READ 事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。
笔者在没有进行这个测试之前,对于幻读的意义理解是停留在类似这种描述上的:
当某个事务在读取某个范围的记录的时候,另外一个事务又在该范围插入了新的记录,当前事务再次读取这个范围的记录,会产生幻行(Phantom Data)-- 《高性能MySQL》第三版
首先这个描述没有问题,笔者之前的理解是:在一个事务中 连续两次查询结果不一致(前提是基于可重复读隔隔离级别下),那这句话的反意就是,在一个事务中,如果连续两次查询结果一致,就不是幻读。
来看下面的案例。
使用 range 查询,IS 锁场景,事务 B 中插入主键间隙之内的一条数据。

在可重复读的隔离级别下,事务 A 满足之前提到的可重复读的情况,不满足前面 在一个事务中 连续两次查询结果不一致 的说法;那么这里对于幻读的解释实际上就是:
事务A 没有正常读取到最新的事务,理论上应该有 3 条数据,而实际查询出来只有 2 条,这种情况对于事务 A 来说产生了幻读?
在事务 A 和事务 B 中插入同一条数据。这种情况,因为在两个事务中同时写入一条数据,当事务 A 写入 id 为 6 数据,但是没有提交事务的时候,理论上事务 B 又写入 id 为 6 的数据会被阻塞住,那么对于事务 B 来说,它就需要知道事务 A 中有同样的操作;来看案例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KoZuYnbL-1668052442693)(https://upload-images.jianshu.io/upload_images/28446384-9b9ca08f04cd1cc0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
此时事务 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 隔离级别下,分为两种情况: