MVCC 和锁是如何最大程度避免幻读的

本文最后更新于 2025年8月27日 16:41

幻读的产生

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

1
SELECT * FROM t_test WHERE id > 100;

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

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

MySQL 中的幻读其实并不能完全解决,但可以通过两种方案来最大程度避免幻读,这两种方案分别是针对 当前读快照读 的,下面依次进行讲解。

快照读如何避免幻读

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

当前读如何避免幻读

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

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

举一个幻读的例子

上面我们提到,这两种方案只是 最大程度避免 了幻读,而非解决了幻读,接下来我们举个例子,看看在什么情况下依然会发生幻读。

image.png

以这张表为例,目前我们有事务 A 和事务 B。

  • 事务 A 执行查询 id = 5 的记录,此时表中是没有该记录的,所以查询不出来;
  • 然后事务 B 插入一条 id = 5 的记录,并且提交了事务;
  • 此时,事务 A 更新 id = 5 这条记录,然后再次查询 id = 5 的记录,事务 A 就能看到事务 B 插入的纪录了。

事务 A 更新 id = 5 这条记录时,它其实并不知道这条记录的存在,但它还是去更新了,幻读就是会发生在这种违和的场景之下。

在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。

总结一下

所以,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行当前读的语句,因为它会对记录加临键锁,从而避免其他事务插入一条新记录。


MVCC 和锁是如何最大程度避免幻读的
http://example.com/2025/04/02/MVCC 和锁是如何最大程度避免幻读的/
作者
Moonike
发布于
2025年4月2日
更新于
2025年8月27日
许可协议