幻读
一、 幻读 (Phantom Read) 是什么?
- 定义: 在同一个事务中,前后两次执行相同的范围查询(当前读),后一次查询看到了前一次查询没有看到的、新插入的行。
- 发生条件:
- 隔离级别: 主要讨论 MySQL 的可重复读 (Repeatable Read, RR) 隔离级别。
- 读取方式: 必须是当前读 (Current Read),例如
SELECT ... FOR UPDATE
,SELECT ... LOCK IN SHARE MODE
,UPDATE
,DELETE
,INSERT
(内部会进行当前读判断唯一性约束等)。 - 普通查询 (快照读): 在 RR 级别下,普通的
SELECT
语句是快照读,基于事务开始时创建的 Read View,不会看到其他事务新插入并提交的行,因此不会出现幻读。
- 与不可重复读的区别: 不可重复读是指同一个事务内,两次读取同一行数据,得到的结果不同(因为被其他事务修改了)。幻读仅指新插入的行被看到了。
二、 幻读有什么问题?
假设 MySQL 在 RR 级别下只使用行锁,不解决幻读问题,会导致:
-
语义破坏:
- 事务 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
的行”的语义被破坏。
- 事务 A 执行
-
数据一致性问题 (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:
- 主库状态: 事务 A 提交时,只会修改它最初锁定的
id=5
的行,id=0
和id=1
的d
值仍然是 5。 - Binlog 顺序 (示例):
- 事务 B 的
UPDATE
- 事务 C 的
INSERT
- 事务 A 的
UPDATE t SET d=100 WHERE d=5;
- 事务 B 的
- 备库/数据恢复: 当备库或通过 Binlog 恢复时,按照上述顺序执行。最后一条
UPDATE
会将所有当前d=5
的行(包括id=0
,id=1
,id=5
)的d
值都修改为 100。 - 结果: 主备数据不一致 (
id=0
,id=1
这两行在主库d=5
,在备库d=100
)。这是严重的数据一致性问题。
- 场景:
三、 InnoDB 如何解决幻读?
- 核心原因: 行锁只能锁定已存在的行,无法阻止新记录插入到行与行之间的“间隙”中。
- 解决方案:引入间隙锁 (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]
。
- 特殊区间: 包含
- 间隙锁 (Gap Lock): 锁定索引记录之间的开区间。例如,对于
- 工作机制:
- 当执行
SELECT ... FOR UPDATE
或其他需要加锁的当前读操作时,在 RR 级别下,InnoDB 不仅会对匹配到的行加行锁,还会对其扫描范围内涉及到的间隙加间隙锁(通常以 Next-Key Lock 的形式)。 - 这样就阻止了其他事务在这些间隙中插入新的记录,从而防止了幻读。
- 当执行
四、 间隙锁/Next-Key Lock 的特性与影响
- 锁冲突:
- 间隙锁 vs 插入: 间隙锁与试图向该间隙插入新记录的操作是冲突的。
- 间隙锁 vs 间隙锁: 不冲突。两个事务可以同时持有同一个间隙的间隙锁。它们的目标都是保护这个间隙不被插入。
- 行锁 vs 行锁: 冲突规则如常(读写、写写冲突)。
- 影响:
- 降低并发度: 因为锁定的范围比单纯的行锁更大(包含了间隙),可能会阻塞更多原本不冲突的操作(特别是插入操作)。
- 增加死锁概率: 典型的死锁场景是:两个事务都想获取同一个间隙的间隙锁(成功,不冲突),然后都尝试向该间隙插入数据,互相等待对方释放间隙锁(但间隙锁只有事务结束才释放),形成死锁。
- 优化: 在某些特定条件下(如唯一索引上的等值查询),Next-Key Lock 会退化为行锁或间隙锁,以提高并发性(详见后续加锁规则)。
五、 替代方案:读提交 (Read Committed) 隔离级别
- 特点: 在 RC 隔离级别下,InnoDB 通常不使用间隙锁(除了外键约束检查等少数情况)。只使用行锁。
- 优点: 并发度更高,死锁概率相对较低。
- 缺点:
- 允许不可重复读和幻读。
- 数据一致性要求: 必须配合
binlog_format = ROW
使用。因为 RC 下 Binlog 如果是 Statement 或 Mixed 格式,主备复制可能出现数据不一致(因为备库执行相同 SQL 时的数据状态可能与主库不同)。ROW 格式记录的是行的实际变更,不受隔离级别影响。
- 适用性: 如果业务逻辑可以接受不可重复读和幻读,并且对并发性能要求较高,RC + ROW Binlog 是一个常见的、合理的选择。但需要明确评估业务需求,不能盲目跟风。
六、 总结
- 幻读是在 RR 隔离级别下,当前读时看到其他事务新插入的行的问题。
- 幻读会导致语义破坏和严重的数据(主备)不一致。
- InnoDB 通过引入间隙锁和 Next-Key Lock 来解决幻读问题,锁住行和间隙。
- 间隙锁/Next-Key Lock 会降低并发并可能引发死锁。
- 读提交隔离级别 + ROW Binlog 是提高并发的一种选择,但牺牲了可重复读保证,需要仔细评估业务需求。