MySQL如何解决幻读问题

Mason
Mason
发布时间:2026-10-16 | 3人浏览过
原创

在MySQL的“可重复读”(Repeatable Read)隔离级别中,“幻读”是事务一致性的核心挑战之一。简单来说,当同一事务在不同时间点读取同一范围的数据时,结果却出现了变化。本文将深入解析InnoDB引擎如何通过多版本并发控制(MVCC)和间隙锁(Gap Lock)的组合机制来消灭幻读现象。

什么是幻读?

假设我们有一张初始数据如下所示的表t:

CREATE TABLE `t`(
    `id` INT(11) NOT NULL,
    `key`  INT(11) DEFAULT NULL,
    `value`  INT(11) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `value` (`value`)
) ENGINE = InnoDB;
INSERT INTO t VALUES (0,0,0),(1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5);

当一个事务在执行 SELECT * FROM t WHERE value=1 FOR UPDATE 这样的当前读操作时,如果其他事务插入了新的符合条件(value=1)的行,那么该事务前后两次查询的结果行数不一致。这种“前一次查询没有,后一次查询出现了新行”的现象,即为幻读。

  • T1时刻: 会话A执行查询Q1,仅返回一行数据 (1,1,1)。
  • T2时刻: 会话B更新 id=0 的记录,使其满足条件(此时共有两行)。注意:单纯的修改不称为幻读。
  • T3时刻: 会话A再次执行查询,发现多了一行 (0,0,1)。
  • T4时刻: 会话C插入了一条完全新的记录 (6,6,1)。
  • T5时刻: 会话A第三次执行查询,发现了这个凭空出现的第三行 (6,6,1)。这就是典型的幻读。

需要区分的是:在“可重复读”隔离级别下,普通的无锁查询(快照读)通过MVCC机制可以看到旧版本的记录,因此通常不会看到别的事务新插入的数据;但一旦使用 FOR UPDATE、LOCK IN SHARE MODE 或 UPDATE/DELETE 等操作进行当前读时,如果没有相应的锁机制保护,幻读就会发生。

为什么需要单独解决?

常规的数据库行锁(Record Lock)只能锁定已经存在的物理记录。然而,新记录的插入是针对记录之间“空隙”的操作,行锁无法覆盖到这些空白的空间。

正如上图所示,即使所有的记录都被加上了行锁,仍然无法阻止外部的线程在这些记录之间插入新的数据。因此,解决幻读必须引入一种能够感知“虚拟”空间的锁机制——即间隙锁(Gap Lock)。

如何解决幻读?

第1步:引入间隙锁(Gap Lock)

InnoDB引入了间隙锁的概念。所谓间隙,就是指两条相邻索引记录之间的空隙。例如在0和5之间存在一个间隙 (0, 5)。

在逐行扫描的过程中,InnoDB不仅会对命中的记录加上“行锁”,还会对行两边的“空隙”加上“间隙锁”。这样就确保了在加锁期间,任何人都不允许将记录插入到被锁定的范围内。

如示例图,因为表中不存在 value=7 的数据,事务1加的间隙锁是 (1, 5),事务2加的也是 (1, 5)。这两个锁互不冲突,目的都是为了防止有人向该空隙内插入数值。

第2步:理解Next-Key Lock机制

为了彻底解决幻读问题,InnoDB采用了间隙锁与行锁结合的 Next-Key Lock 机制。每个Next-Key Lock是一个前开后闭区间(左开右闭),既包含了左边的间隙,也包含了右边的记录本身。

如果在表t上使用 SELECT * FROM t FOR UPDATE 对所有数据进行扫描加锁,形成的Lock区间将是:
(-∞,0]、(0,2]、(2,4]、(4,6]、(6,8]、(8, 10]、(10, +supremum]

这意味着所有可能插入新数据的逻辑区间全部被封锁,从而从根本上杜绝了幻读的发生。

第3步:处理锁引发的死锁隐患

虽然Next-Key Lock解决了幻读,但它可能会过度锁定,导致并发度降低甚至引发死锁。

死锁场景演示:
1. 会话A执行 SELECT ... FOR UPDATE 查找 id=9(该行不存在),获得间隙锁 (8,10)。
2. 会话B同样查找 id=9,获得相同的间隙锁 (8,10)(间隙锁不冲突)。
3. 此时,会话B试图插入 (9,9,9),但由于遇到了会话A持有的间隙锁,进入等待状态。
4. 同时,会话A也想插入 (9,9,9),但却遭遇了会话B持有的间隙锁,同时也进入等待状态。
这就构成了经典的循环死锁。

总结与最佳实践建议

InnoDB通过以下组合拳完美解决了RR隔离级别下的幻读问题:

  • 快照读(普通SELECT): 依赖MVCC多版本机制保证一致性,不阻塞写操作。
  • 当前读(FOR UPDATE / 增删改): 依靠Next-Key Lock(间隙锁+行锁)锁定目标范围及周围的空隙。

排错与建议:
如果在实际业务中发现间隙锁导致的并发度极低,且能够接受一定程度的数据最终一致性,可以考虑调整隔离级别为“读已提交”(Read Committed, RC)。在RC级别下,间隙锁是不起作用的。但这要求必须将Binlog日志格式设置为 row,以防止主从复制时产生日志不一致的问题(这也是目前大多数云厂商默认的推荐配置)。

以上就是关于MySQL可重复读隔离级别下如何解决幻读的详细内容,更多关于MySQL 隔离级别机制的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表