MVCC 和锁是如何最大程度避免幻读的
本文最后更新于 2025年8月27日 16:41
幻读的产生
举个例子,假设一个事务在 T1 时刻和 T2 时刻分别执行了下面查询语句,途中没有执行其他任何语句:
1 |
|
只要 T1 和 T2 时刻执行产生的结果集是不相同的,那就发生了幻读的问题,比如:
- T1 时间执行的结果是有 5 条记录,而 T2 时间执行的结果是有 6 条记录,那就发生了幻读。
- T1 时间执行的结果是有 5 条记录,而 T2 时间执行的结果是有 4 条记录,也是发生了幻读。
MySQL 中的幻读其实并不能完全解决,但可以通过两种方案来最大程度避免幻读,这两种方案分别是针对 当前读 和 快照读 的,下面依次进行讲解。
快照读如何避免幻读
针对快照读(普通 select
语句),是通过 MVCC 方式解决了幻读。因为在可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
当前读如何避免幻读
MySQL 里除了普通查询是快照读,其他都是当前读,比如 select ... for update
、update
、insert
、delete
,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。
针对当前读,是通过 next-key lock(临键锁)方式解决了幻读。因为执行当前读的时候,会加上临键锁,如果有其他事务在临键锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
举一个幻读的例子
上面我们提到,这两种方案只是 最大程度避免 了幻读,而非解决了幻读,接下来我们举个例子,看看在什么情况下依然会发生幻读。
以这张表为例,目前我们有事务 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 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行当前读的语句,因为它会对记录加临键锁,从而避免其他事务插入一条新记录。