前言

前置知识:MySQL 是怎么加锁的?

MySQL InnoDB 引擎的默认隔离级别是「可重复读」,简称RR,RR隔离级别会存在幻读现象,Mysql是用两种方式解决的:

  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。

  • 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

一、什么是幻读?

当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题(更偏向于「记录数量」)。例如,如果 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是“幻像”行。

举个例子,假设一个事务在 T1 时刻和 T2 时刻分别执行了下面查询语句,途中没有执行其他任何语句:

SELECT * FROM t_test WHERE id > 100;

只要 T1 和 T2 时刻执行产生的结果集是不相同的,那就发生了幻读的问题,比如:

  • T1 时间执行的结果是有 5 条行记录,而 T2 时间执行的结果是有 6 条行记录,那就发生了幻读的问题。

  • T1 时间执行的结果是有 5 条行记录,而 T2 时间执行的结果是有 4 条行记录,也是发生了幻读的问题。

二、快照读是如何避免幻读的?

可重复读隔离级是由 MVCC(多版本并发控制)实现的,实现的方式是启动事务后,在执行第一个查询语句后,会创建一个 Read View,后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入了新纪录,是查询不出来这条数据的,所以就很好了避免幻读问题。

做个实验,数据库表 t_stu 如下,其中 id 为主键。

1-amqv.png

然后在可重复读隔离级别下,有两个事务的执行顺序如下:

2-sgib.png

从这个实验结果可以看到,即使事务 B 中途插入了一条记录,事务 A 前后两次查询的结果集都是一样的,并没有出现所谓的幻读现象。

三、当前读是如何避免幻读的?

MySQL 里除了普通查询是快照读,其他都是当前读,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。

Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了间隙锁

假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。

举个例子:

3-whyn.png

四、幻读被彻底解决了吗?

4.1 场景一

4-kpbx.png

由于update是当前读,所以此时会读取最新的数据(包括其他已经提交的事务)

4.2 场景二

  • 事务 A 先执行【快照读语句】:select * from t_test where id > 100 得到了3条记录。

  • 事务 B 插入一个 id = 200 的记录并提交。

  • 事务 A 再执行【当前读语句】: select * from t_test where id > 100 for update 就会得到4条记录,此时也发生了幻读现象。

所以,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生

要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... For update 这类当前读的语句,因为它会对记录加 next-key lock ,从而避免其他事务插入一条新记录。