Mysql数据库幻读问题举例详解

 更新时间:2025年10月11日 11:19:43   作者:倒悬于世  
数据库幻读是数据库并发事务控制中可能发生的一种现象,它属于不可重复读的一个特例,但关注点不同,这篇文章主要介绍了Mysql数据库幻读问题的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

我们来详细地聊一聊 MySQL InnoDB 中的“幻读”(Phantom Read)问题。这是一个在数据库事务隔离中非常核心且有时令人困惑的概念。

我会从定义、例子、原因以及解决方案几个方面来彻底讲清楚。

1. 什么是幻读?

官方定义:幻读指的是在一个事务内,相同的查询在不同时间执行,返回了不同数量的行

这听起来和“不可重复读”很像,但它们有关键区别:

  • 不可重复读 (Non-Repeatable Read):侧重于同一行的数据内容被修改或删除。(例如:你第二次查询时,某行的薪水从10000变成了12000)。

  • 幻读 (Phantom Read):侧重于新的行被插入(或删除),导致结果集的行数发生了变化。(针对结果集的数量变化,例如:你第一次查询有10条记录,第二次查询却冒出了11条)。

简单比喻

  • 不可重复读:你碗里的一块红烧肉被别人咬了一口(数据内容变了)。

  • 幻读:你正准备夹碗里最后一块红烧肉时,别人突然又往碗里加了一块新的肉(数据行数变了)。

2. 幻读发生的场景与例子

幻读发生的根本原因是:在“可重复读(REPEATABLE READ)”及以下隔离级别中,普通的一致性读(快照读)无法阻止其他事务插入新的、满足当前查询条件的数据

我们来看一个经典的例子。

表结构

CREATE TABLE `employee` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `salary` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_salary` (`salary`)
) ENGINE=InnoDB;

INSERT INTO `employee` (`name`, `salary`) VALUES
('Alice', 8000),
('Bob', 9000),
('Charlie', 10000);

时间线

时间事务A (隔离级别:RR)事务B
T1START TRANSACTION;
T2SELECT * FROM employee WHERE salary > 8000;
结果
Bob, 9000
Charlie, 10000
(2 rows)
START TRANSACTION;
T3INSERT INTO employee (name, salary) VALUES ('David', 9500);
COMMIT; <!-- 事务B提交 -->
T4SELECT * FROM employee WHERE salary > 8000;
结果
Bob, 9000
Charlie, 10000
(仍然是2 rows!)
(这里没有幻读,因为RR级别通过MVCC提供了快照)
T5UPDATE employee SET salary = 8888 WHERE salary > 8000;
(注意:这个更新操作是当前读,会看到事务B已提交的修改)
T6SELECT * FROM employee WHERE salary > 8000;
结果
Bob, 8888
Charlie, 8888
David, 8888
(3 rows! 幻读出现了!)

例子分析

  1. T2时刻:事务A第一次查询,得到2条记录。

  2. T4时刻:事务A第二次普通查询(快照读)。由于InnoDB的MVCC(多版本并发控制)机制,它会读取事务开始时的数据快照,所以看不到事务B新插入的 David(9500)。此时还没有幻读。

  3. T5时刻:关键点来了!事务A执行了一个UPDATE操作。UPDATE/DELETE/INSERT 这类写操作会使用“当前读”(Current Read),它会读取数据库中最新的、已提交的数据。因此,它看到了事务B插入的 David(9500) 这条记录,并将其薪水也更新为8888。

  4. T6时刻:事务A再次查询。因为之前的UPDATE操作属于当前事务的修改,所以MVCC规则允许它看到自己的修改。于是,它神奇地看到了三条记录!幻读就在这一刻发生了

这个例子展示了InnoDB中幻读最典型的特征:即使是在默认的RR隔离级别下,先快照读,再当前读进行写操作,可能会意外地影响新插入的行,从而导致数据不一致。

3. 解决方案:Next-Key Lock 锁机制

InnoDB引擎为了解决幻读问题,在“可重复读(REPEATABLE READ)”隔离级别下就引入了一种叫做 Next-Key Lock 的锁机制。它实际上是 记录锁(Record Lock) 和 间隙锁(Gap Lock) 的结合。

  • 记录锁 (Record Lock):锁住索引上的某一条具体记录。

  • 间隙锁 (Gap Lock):锁住索引记录之间的“间隙”,防止在这个间隙内插入新的数据。它是一个左开右开的区间 (a, b)

  • 临键锁 (Next-Key Lock):是记录锁 + 间隙锁的结合。它锁住一条记录和它前面的间隙。它是一个左开右闭的区间 (a, b]

如何解决幻读?

在上面的例子中,如果事务A在第一次查询时,就对 salary > 8000 这个条件加上了锁,那么事务B的插入操作就会被阻塞,从而杜绝幻读。

让我们重演时间线,但这次事务A加锁查询

时间事务A (加锁查询)事务B
T1START TRANSACTION;
T2SELECT * FROM employee WHERE salary > 8000 FOR UPDATE;
(FOR UPDATE 会给查询结果加Next-Key Lock)
结果:2 rows
START TRANSACTION;
T3INSERT INTO employee (name, salary) VALUES ('David', 9500);
(这条语句会被阻塞,一直等待事务A释放锁!)
T4SELECT ... FOR UPDATE; (再次查询,结果一致)...(阻塞中)...
T5COMMIT; (提交事务,释放锁)...(阻塞结束)...
T6(此时事务B才能成功插入)

发生了什么?

当事务A执行 SELECT ... FOR UPDATE 时,InnoDB会为其加Next-Key Lock。假设 salary 上有二级索引 idx_salary,它可能会锁住以下区间:

  • 锁住 (8000, 9000] 这个Next-Key Lock(锁住9000这条记录和它前面的间隙)。

  • 锁住 (9000, 10000] 这个Next-Key Lock(锁住10000这条记录和它前面的间隙)。

  • 锁住 (10000, +∞] 这个Next-Key Lock(锁住正无穷的上界)。

事务B试图插入 salary = 9500 的记录,这个值落在被事务A锁住的 (9000, 10000] 间隙内,因此插入操作会被阻塞,直到事务A提交释放锁。这样就彻底防止了幻读的发生。

总结与最佳实践

特性说明
幻读本质同一事务内,两次查询结果集行数不一致, due to 其他事务的插入删除操作。
InnoDB的默认防御REPEATABLE READ隔离级别下,InnoDB通过 Next-Key Lock 机制来防止幻读。
何时会发生幻读即使是在RR级别下,如果你只是进行普通的快照读(SELECT),然后基于此进行当前读的写操作(UPDATE/INSERT/DELETE),仍然可能遇到幻读。快照读不加锁是根源。
彻底解决方法在需要绝对保证数据一致性的关键操作中,使用 加锁读
1. SELECT ... FOR UPDATE; (加写锁,阻塞其他事务的写和加锁读)
2. SELECT ... LOCK IN SHARE MODE; (加读锁,阻塞其他事务的写)
这些语句会在符合条件的索引上加Next-Key Lock,从而阻止其他事务在锁定区间内插入新数据。
终极方案将事务隔离级别提升至 SERIALIZABLE。在这个级别下,所有的读操作都会默认加上类似 LOCK IN SHARE MODE 的锁,幻读自然不会发生,但这是以牺牲并发性能为代价的,一般不建议使用。

核心要点:MySQL InnoDB 在 RR 级别下已经通过 Next-Key Lock 很大程度上解决了幻读问题。但你需要清楚地知道,只有在你的查询语句确实需要加锁(例如使用了 FOR UPDATE)或者涉及写操作时,Next-Key Lock 才会生效。单纯的快照读是无法完全避免幻读的潜在影响的。

到此这篇关于Mysql数据库幻读问题的文章就介绍到这了,更多相关Mysql幻读详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • mysql如何定时自动新增分区

    mysql如何定时自动新增分区

    这篇文章主要介绍了mysql如何定时自动新增分区问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09
  • 详解Windows Server 2012下安装MYSQL5.7.24的问题

    详解Windows Server 2012下安装MYSQL5.7.24的问题

    这篇文章主要介绍了Windows Server 2012下安装MYSQL5.7.24的详细过程,本文通过图文并茂实例代码相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-02-02
  • mybatis分页插件pageHelper详解及简单实例

    mybatis分页插件pageHelper详解及简单实例

    这篇文章主要介绍了mybatis分页插件pageHelper详解及简单实例的相关资料,需要的朋友可以参考下
    2017-05-05
  • SQL优化教程之in与range查询

    SQL优化教程之in与range查询

    这篇文章主要介绍了给大家介绍了SQL优化之in与range查询的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • 如何设置才能远程登录Mysql数据库

    如何设置才能远程登录Mysql数据库

    本地机器安装的数据库,本地程序可以访问,但是同事的机器却无法连接访问,发现是mysql数据库没有开启远程访问。那么我们需要如何设置呢,这就是本文探讨的内容了
    2014-08-08
  • MySQL 内存表和临时表的用法详解

    MySQL 内存表和临时表的用法详解

    这篇文章主要介绍了MySQL 内存表和临时表的用法详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-01-01
  • 解决Mysql多行子查询的使用及空值问题

    解决Mysql多行子查询的使用及空值问题

    所谓多行子查询,是指执行查询语句获得的结果集中返回了多行数据的子查询,今天通过本文给大家分享Mysql多行子查询的使用及空值问题,感兴趣的朋友一起看看吧
    2022-01-01
  • 简单分析MySQL中的primary key功能

    简单分析MySQL中的primary key功能

    这篇文章主要介绍了MySQL中的primary key功能,包括讲到了其对InnoDB使用的影响,需要的朋友可以参考下
    2015-05-05
  • MySQL修改默认存储引擎的实现方法

    MySQL修改默认存储引擎的实现方法

    下面小编就为大家带来一篇MySQL修改默认存储引擎的实现方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-03-03
  • 3步搞定纯真IP数据导入到MySQL的方法详解

    3步搞定纯真IP数据导入到MySQL的方法详解

    免编程,3步搞定纯真IP数据导入到MySQL详解,好多做ip地址查询的朋友用的到。
    2009-10-10

最新评论