bigywater.tech
MySQL / InnoDB · 51

Phantom Read 幻读

在同一个事务里对同一个范围数两次行数,中途另一个会话插入一行新数据,看普通读和当前读对这行'幻影'的真实态度有什么不同。

8→8
同一事务内两次普通COUNT,真实都没看见新插入的行
8→9
换成FOR UPDATE后,真实立刻数到了那一行

做的事:同一个事务里,两次普通COUNT,中间插入一行新数据

session A(REPEATABLE READ)START TRANSACTION; SELECT COUNT(*) FROM accounts WHERE id BETWEEN 10 AND 90; -- 第一次 SELECT SLEEP(3); SELECT COUNT(*) FROM accounts WHERE id BETWEEN 10 AND 90; -- 第二次(B已插入并提交) SELECT COUNT(*) FROM accounts WHERE id BETWEEN 10 AND 90 FOR UPDATE; -- 第三次,当前读 COMMIT; -- session B(在A的SLEEP期间执行) INSERT INTO accounts VALUES (85, 1000, 85);

验证:两次普通COUNT真实都是8,B插入的新行真实"看不见"

实测 · 本机两个并发会话的真实输出
第一次普通COUNT:                     8
(B执行 INSERT id=85 并提交)
第二次普通COUNT(同一事务内):         8   ← 真实没变,B插入的新行不在快照里
第三次 FOR UPDATE(当前读):           9   ← 真实变了,看见了B插入的这一行

两次普通COUNT(*)真实完全一样,这正是REPEATABLE READ隔离级别对幻读的真实防护——普通SELECT基于事务开始时的快照,B在这之后插入的新行,不管有没有提交,对A的一致性读来说都真实"不存在"。换成FOR UPDATE做当前读之后,真实立刻数出了9条,B插入的那一行(id=85)变得可见了——当前读没有快照保护,读到的是执行这条语句那一刻真实已经提交的最新数据,包括别的事务刚插入的新行。

结论:InnoDB用间隙锁堵住了当前读那一侧的幻读,但普通读本来就不会看见

教科书里常说"InnoDB的REPEATABLE READ通过间隙锁解决了幻读",这句话真实只对了一半:普通SELECT从一开始就不会有幻读问题,因为它压根不去读最新数据,读的是快照,B插入的新行自然不可见,这跟间隙锁没有任何关系。间隙锁真实解决的是当前读场景下的幻读——如果A在第一次COUNT之后紧接着就用FOR UPDATE锁住id BETWEEN 10 AND 90这个范围(间隙锁),B的INSERT会真实被阻塞而不是成功插入,这样A之后再用FOR UPDATE读,才能保证和之前锁定时看到的结果一致。本文这个例子里A的第一次读是普通SELECT,没有加任何锁,所以B的INSERT真实畅通无阻。

分步演示:普通读为什么从一开始就没有幻读

每一步都是本文上面用两个真实并发会话跑出来的原始结果。

幻读只在当前读那一侧真实出现
这一步新验证的读取

    本文用到的工具

    • 本机 MySQL 26.7.0(InnoDB),两个真实并发会话,REPEATABLE READ隔离级别
    • 普通SELECT COUNT(*)(一致性读) vs SELECT ... FOR UPDATE(当前读)
    京ICP备2026067502号-1京公网安备11010502063487号