可重复读隔离级别是如何避免幻读的

15 分钟阅读 1283 字 + 1548 词
MySQL InnoDB 引擎的默认隔离级别虽然是==「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了)==,解决的方案有两种:
  • 针对== 快照读 ==( 普通 select 语句 ),是== 通过 MVCC 方式解决了幻读 ==,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
  • 针对== 当前读 ==( select ... for update 等语句(加锁的语句)),是== 通过 next-key lock(记录锁+间隙锁)方式解决了幻读 ==,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
快照读:单纯的 select 操作。读取的是快照(ReadView)中的数据,可能是历史数据
当前读:select ... for update/in share mode、update、insert、delete。读取的总是当前的最新数据
对于快照读,RR中一个事务的所有快照读读取的都是同一份快照,所以无论其他的事务怎么修改,无论是更新还是插入删除,都不会影响当前事务的快照读结果,也就不会出现 不可重复读 、幻读的情形。
对于当前读,你读取的行,以及行的间隙都会被加锁,直到事务提交时才会释放,其他的事务无法进行修改,所以也不会出现不可重复读、幻读的情形。
所以如果你总是进行快照读,或者总是进行当前读,是不会出现幻读的情况的。(如果同时出现快照读和当前读,则可能幻读,下文的两个场景都是这种情况)
快照读是如何避免幻读的?
可重复读隔离级是由 MVCC(多版本并发控制)实现的,实现的方式是启动事务后,在执行第一个查询语句后,会创建一个 Read View, 后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的 ,即使中途有其他事务插入了新纪录,是查询不出来这条数据的,所以就很好了避免幻读问题。
当前读是如何避免幻读的?
MySQL 里除了普通查询是快照读,其他都是 当前读 ,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。
这很好理解,假设你要 update 一个记录,另一个事务已经 delete 这条记录并且提交事务了,这样不是会产生冲突吗,所以 update 的时候肯定要知道最新的数据。
另外, select ... for update 这种查询语句是当前读,每次执行的时候都是读取最新的数据。
接下来,我们假设 select ... for update 当前读是不会加锁的(实际上是会加锁的),在做一遍实验。
img
所以, Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了间隙锁
幻读被完全解决了吗?
可重复读隔离级别下虽然很大程度上避免了幻读,但是还是没有能完全解决幻读
我举例一个可重复读隔离级别发生幻读现象的场景。
第一个发生幻读现象的场景
还是以这张表作为例子:
img
事务 A 执行查询 id = 5 的记录,此时表中是没有该记录的,所以查询不出来。
sql
# 事务 A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_stu where id = 5;
Empty set (0.01 sec)
然后事务 B 插入一条 id = 5 的记录,并且提交了事务。
sql
# 事务 B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t_stu values(5, '小美', 18);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
此时, 事务 A 更新 id = 5 这条记录,对没错,事务 A 看不到 id = 5 这条记录,但是他去更新了这条记录,这场景确实很违和,然后再次查询 id = 5 的记录,事务 A 就能看到事务 B 插入的纪录了,幻读就是发生在这种违和的场景
mysql
# 事务 A
mysql> update t_stu set name = '小林coding' where id = 5;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t_stu where id = 5;
+----+--------------+------+
| id | name         | age  |
+----+--------------+------+
|  5 | 小林coding   |   18 |
+----+--------------+------+
1 row in set (0.00 sec)
img
第二个发生幻读现象的场景
除了上面这一种场景会发生幻读现象之外,还有下面这个场景也会发生幻读现象。
  • T1 时刻:事务 A 先执行「 快照读语句 」:select * from t_test where id > 100 得到了 3 条记录。
  • T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
  • T3 时刻:事务 A 再执行「 当前读语句 」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。
要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句 ,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。