Skip to content

幻读

一、 幻读 (Phantom Read) 是什么?

  1. 定义:同一个事务中,前后两次执行相同的范围查询(当前读),后一次查询看到了前一次查询没有看到的、新插入的行
  2. 发生条件:
    • 隔离级别: 主要讨论 MySQL 的可重复读 (Repeatable Read, RR) 隔离级别。
    • 读取方式: 必须是当前读 (Current Read),例如 SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, UPDATE, DELETE, INSERT (内部会进行当前读判断唯一性约束等)。
    • 普通查询 (快照读): 在 RR 级别下,普通的 SELECT 语句是快照读,基于事务开始时创建的 Read View,不会看到其他事务新插入并提交的行,因此不会出现幻读。
  3. 与不可重复读的区别: 不可重复读是指同一个事务内,两次读取同一行数据,得到的结果不同(因为被其他事务修改了)。幻读仅指新插入的行被看到了。

二、 幻读有什么问题?

假设 MySQL 在 RR 级别下只使用行锁,不解决幻读问题,会导致:

  1. 语义破坏:

    • 事务 A 执行 SELECT * FROM t WHERE d=5 FOR UPDATE; 的意图是锁定所有 d=5 的行,防止其他事务修改或插入满足此条件的行。
    • 如果只锁住已存在的行(如 id=5),事务 B 仍然可以更新其他行(如 id=0)使其 d 变为 5,或者事务 C 可以插入新的 d=5 的行(如 id=1)。
    • 这违背了事务 A 最初的加锁声明,即“锁住所有 d=5 的行”的语义被破坏。
  2. 数据一致性问题 (Binlog 相关):

    • 场景:
      • 事务 A: UPDATE t SET d=100 WHERE d=5; (意图修改所有 d=5 的行)
      • 事务 B (在 A 提交前): UPDATE t SET d=5 WHERE id=0; COMMIT;
      • 事务 C (在 A 提交前): INSERT INTO t VALUES(1,1,5); COMMIT;
    • 主库状态: 事务 A 提交时,只会修改它最初锁定的 id=5 的行,id=0id=1d 值仍然是 5。
    • Binlog 顺序 (示例):
      1. 事务 B 的 UPDATE
      2. 事务 C 的 INSERT
      3. 事务 A 的 UPDATE t SET d=100 WHERE d=5;
    • 备库/数据恢复: 当备库或通过 Binlog 恢复时,按照上述顺序执行。最后一条 UPDATE 会将所有当前 d=5 的行(包括 id=0, id=1, id=5)的 d 值都修改为 100。
    • 结果: 主备数据不一致 (id=0, id=1 这两行在主库 d=5,在备库 d=100)。这是严重的数据一致性问题

三、 InnoDB 如何解决幻读?

  1. 核心原因: 行锁只能锁定已存在的行,无法阻止新记录插入到行与行之间的“间隙”中。
  2. 解决方案:引入间隙锁 (Gap Lock) 和 Next-Key Lock
    • 间隙锁 (Gap Lock): 锁定索引记录之间的开区间。例如,对于 id 为 0, 5, 10 的记录,存在间隙 (0, 5), (5, 10) 等。
    • Next-Key Lock: 行锁 + 该行之前的间隙锁的组合,形成一个前开后闭的区间。例如,对于 id=5 的记录,其 Next-Key Lock 覆盖范围是 (0, 5]。InnoDB 使用 Next-Key Lock 作为加锁的基本单位(在 RR 级别下)。
      • 特殊区间: 包含 (-∞, min_value](max_value, +supremum]
  3. 工作机制:
    • 当执行 SELECT ... FOR UPDATE 或其他需要加锁的当前读操作时,在 RR 级别下,InnoDB 不仅会对匹配到的加行锁,还会对其扫描范围内涉及到的间隙加间隙锁(通常以 Next-Key Lock 的形式)。
    • 这样就阻止了其他事务在这些间隙中插入新的记录,从而防止了幻读。

四、 间隙锁/Next-Key Lock 的特性与影响

  1. 锁冲突:
    • 间隙锁 vs 插入: 间隙锁与试图向该间隙插入新记录的操作是冲突的。
    • 间隙锁 vs 间隙锁: 不冲突。两个事务可以同时持有同一个间隙的间隙锁。它们的目标都是保护这个间隙不被插入。
    • 行锁 vs 行锁: 冲突规则如常(读写、写写冲突)。
  2. 影响:
    • 降低并发度: 因为锁定的范围比单纯的行锁更大(包含了间隙),可能会阻塞更多原本不冲突的操作(特别是插入操作)。
    • 增加死锁概率: 典型的死锁场景是:两个事务都想获取同一个间隙的间隙锁(成功,不冲突),然后都尝试向该间隙插入数据,互相等待对方释放间隙锁(但间隙锁只有事务结束才释放),形成死锁。
  3. 优化: 在某些特定条件下(如唯一索引上的等值查询),Next-Key Lock 会退化为行锁或间隙锁,以提高并发性(详见后续加锁规则)。

五、 替代方案:读提交 (Read Committed) 隔离级别

  1. 特点: 在 RC 隔离级别下,InnoDB 通常不使用间隙锁(除了外键约束检查等少数情况)。只使用行锁。
  2. 优点: 并发度更高,死锁概率相对较低。
  3. 缺点:
    • 允许不可重复读和幻读。
    • 数据一致性要求: 必须配合 binlog_format = ROW 使用。因为 RC 下 Binlog 如果是 Statement 或 Mixed 格式,主备复制可能出现数据不一致(因为备库执行相同 SQL 时的数据状态可能与主库不同)。ROW 格式记录的是行的实际变更,不受隔离级别影响。
  4. 适用性: 如果业务逻辑可以接受不可重复读和幻读,并且对并发性能要求较高,RC + ROW Binlog 是一个常见的、合理的选择。但需要明确评估业务需求,不能盲目跟风。

六、 总结

  • 幻读是在 RR 隔离级别下,当前读时看到其他事务新插入的行的问题。
  • 幻读会导致语义破坏和严重的数据(主备)不一致。
  • InnoDB 通过引入间隙锁和 Next-Key Lock 来解决幻读问题,锁住行和间隙。
  • 间隙锁/Next-Key Lock 会降低并发并可能引发死锁。
  • 读提交隔离级别 + ROW Binlog 是提高并发的一种选择,但牺牲了可重复读保证,需要仔细评估业务需求。