为什么MySQL可重复读级别不能完全避免幻读
可重复读隔离级别通过 MVCC 快照读避免不可重复读,而阻止幻读依赖于在“当前读”时加的间隙锁;因为快照读不加锁,间隙锁只在执行当前读时才可能会生效,——所以 RR 无法 100% 消除幻读。
一致性读(快照读)
基于事务的 Read View(MVCC 快照),不加锁,读到的是事务启动时或语句启动时的可见版本。
/*一致性读(快照读)
*/
-- 普通 SELECT
SELECT * FROM account WHERE id = 1;
-- 范围查询
SELECT * FROM account WHERE balance > 1000;
-- 聚合查询
SELECT COUNT(*) FROM account WHERE balance > 1000;
当前读
读取当前已提交的数据并在读取时加锁,用于保证随后的写操作安全或防幻读。
/*当前读
*/-- 排它锁(锁住记录 + 间隙)
SELECT * FROM account WHERE id BETWEEN 10 AND 20 FOR UPDATE;-- 共享锁(锁住记录 + 间隙,但允许其他事务读取)
SELECT * FROM account WHERE id BETWEEN 10 AND 20 LOCK IN SHARE MODE;-- 更新指定范围的记录(隐式当前读 + 加锁)
UPDATE account SET balance = balance - 100 WHERE id BETWEEN 10 AND 20;-- 删除操作(隐式当前读 + 加锁)
DELETE FROM account WHERE balance < 500;
要点说明
MVCC 与快照读的本质:
RR 下普通 SELECT 使用事务启动时的 Read View(快照),读的是历史版本,不受后来提交的写影响,快照读本身不加锁,因此并不会阻止并发事务插入新行(这些新行对快照不可见,但如果随后做当前读就可能看到它们)。
间隙锁 / Next-Key Lock 的作用与时机:
为防幻读,InnoDB 在执行当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)时对记录及其间隙加锁(next-key lock = 记录锁 + 间隙锁)。
加锁是在执行当前读时发生,只能阻止之后的插入,无法阻止在此之前已经插入的行。
索引依赖与锁覆盖问题:
间隙锁基于索引范围;如果查询未走合适索引或条件复杂,锁可能覆盖不全或退化,导致仍有插入不被拦截,从而出现幻读。
快照读与当前读混合的语义差异:
事务内先做快照读(看到旧数据),随后做当前读(看到最新并加锁),两者语义不同,可能产生“集合变化”的感受,即幻读现象仍然可能出现。
性能与一致性的权衡:
InnoDB 不会无差别地锁住所有间隙(会权衡并发性能),因此在某些场景下不会或不能完全阻止所有可能的幻读。
RR下发生幻读的示例
-- T1: 第一次范围查询
SELECT * FROM account WHERE balance > 1000;
-- 普通 SELECT(快照读 / 一致性读)
-- 不加锁,不触发间隙锁
-- 只能看到事务启动时的快照-- T2: 插入新记录
INSERT INTO account(id, balance) VALUES (1001, 2000);
-- 可以成功插入,因为 T1 的第一次查询没有加锁-- 回到 T1: 第二次查询
SELECT * FROM account WHERE balance > 1000 FOR UPDATE;
-- 当前读,此时才会触发锁(排它锁 + 间隙锁)
-- 会读取最新数据,包括 T2 插入的记录,就会看到新增行 → 发生幻读
-- 如果后面再有事务对已经锁住的区域进行写操作,就会阻塞