Innodb引擎可重复读的幻读现象

关于幻读

众所周知,MySQL不同的隔离级别可能会出现不同的问题。其中,有一问题就是在可重复读隔离级别会出现幻读问题。可能会有人说:不是说MySQL的可重复读隔离级不会出现幻读吗?怎么会出现幻读?

这里有2个问题需要明确:
① MySQL官方并未说明可重复读隔离级别不会出现幻读
② 什么是幻读

问题① 查看MySQL官方相关资料,官方也会标明可重复读隔离级别可能会出现幻读
问题② 要理解幻读是可见与不可见的一种权衡。如果用一句话说明,那就是:幻读是看不到数据,却能感受到它存在的一种现象

产生幻读的本质是不同读的方式:当前读 or 快照读,这两种方式的选择

数据库中数据操作方式,一般分为如下四种:
1.SELECT
2.INSERT
3.UPDATE
4.DELETE

在可重复读隔离级别中,SELECT 操作不会对数据造成影响,剩余的三种操作则会对数据造成影响。并且,这几种操作看待数据的方式也不太一样。
SELECT 使用快照读,剩余三种操作使用当前读

活跃事务数组与可见性

在事务开启之后,每个事务会持有一个当前活跃事务数组,活跃事务数组中保存着当前存在却还未提交的事务ID。对这些事务ID进行排序,排序后可以看到事务数组的最小事务ID与最大事务ID。

由于 MySQL中MVCC(多版本并发控制)的存在,在每一行数据中会隐式的保存影响当前数据的最后一个事务ID。所以就有一些规则来限制事务能够看到哪些数据以及看不到哪些数据。

例如:当前事务ID为 100,看到的活跃事务数组为 [98, 100, 104, 110]。那么,该事务 SELECT 操作看数据的规则如下:
① 能够看到事务ID小于98的数据 (事务ID小于98的数据在当前事务开启前已经提交)
② 看不到事务ID大于110的数据(事务ID大于110的数据是在当前事务启动之后出现的,对当前事务来说,未来要发生的看不到)
③ 看不到活跃事务数组中除自己之外其他事务操作的数据(看到自己操作的数据,其他的看不到,是因为其他事务还未提交)
④ 可以看到大于活跃事务数组最小值,小于活跃事务数组最大值,不存在活跃事务数组中其他事务ID操作的数据(这些事务ID的数据在当前事务开启前已经提交,所以能看到)

在整个事务过程中,每次操作都使用在事务开启时保存的活跃事务数组,这种数据读取方式为 快照读
在整个事务过程中,每次操作都使会生成新的活跃事务数组并使用它,这种数据读取方式为 当前读

可想而知,在可重复读隔离级别中 SELECT 看到的数据能够一直不发生变化,是因为 快照读 的功劳。但是,如果在整个事务操作过程中,当前事务发生了数据的更新操作,就会看到一些不应该看到的数据,即发生了 幻读

可重复读隔离级别幻读演示

创建演示表

1
2
3
4
5
6
7
8
mysql> show create table t1\G;

*************************** 1. row ***************************
Create Table: CREATE TABLE `t1` (
`id` tinyint(4) NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT 操作产生的幻读

事务A第10步 insert 插入在事务A看来本不应该存在的数据,却因为数据已经存在发生冲突而失败,可以视为事务A发生了幻读。

UPDATE 操作产生的幻读

事务A的第10步 成功update更新了自己看不到的数据,可以视为事务A发生了幻读。
事务A的第11步 看到了前面看不到的数据(此时能够看到,是因为id=3的数据中保存的最后一个事务id已经变为当前事务的id)

DELETE 操作产生的幻读

事务A的第10步操作中成功delete删除了自己看不到的数据,可以视为事务A发生了幻读。

通过上面操作,可以看出在可重复读隔离级别中,同一个中事物 SELECT 操作查看数据数据一直使用 快照读,而 INSERT、UPDATE、DELETE 操作却由于是 当前读,会产生幻读现象。

加锁读

锁一般分为:读、写锁,读写、写写互斥,读读则并存。

\ 读锁 写锁
读锁 共存 互斥
写锁 互斥 互斥

常见的加锁读操作语句有如下几种:

1
2
3
4
SELECT * FROM table_name FOR SHARE ; -- 加 共享读锁
SELECT * FROM table_name FOR UPDATE; -- 加 排它独占锁

SELECT * FROM table_name LOCK IN SHARE MODE; -- 加 共享读锁 的另一种写法

官方文档 https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html

可重复读隔离级别 中如果存在更新数据的操作,为了防止幻读,一般会根据业务需要选择对应的加锁语句,不同的SQL语句会产生不同的锁(行锁、表锁、间隙锁、行锁+间隙锁)。在加锁之后,会阻塞其他事务操作对应数据行,从而避免产生 幻读 现象。

推荐阅读

15.7.2 InnoDB Transaction Model

https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-model.html