你正在开发一个会议室预定系统。事务 A查询 10:00 - 11:00 有没有空闲的会议室SELECT * FROM rooms WHERE status FREE。结果显示没有。事务 B此时另一个管理员插入了一条新记录10:00 - 11:00 的“Room A”是空闲的。事务 A再次查询或者准备插入一条冲突记录时突然发现——怎么多出来一个空闲会议室灵异现象刚才明明检查过“没有”转眼间又“有”了。你以为自己看花眼了其实是撞上了数据库的幻读。1. 核心定义幻读 vs 不可重复读这是面试和实战中最容易混淆的概念。不可重复读 (Non-Repeatable Read):重点在修改 (UPDATE) 或 删除 (DELETE)。场景第一次查是 100 块钱第二次查变成 0 块钱了被别人改了。锁定策略锁住这一行记录即可解决。幻读 (Phantom Read):重点在插入 (INSERT)。场景第一次查有 3 行记录第二次查变成了 4 行记录被别人插进来了。锁定策略光锁住现有的行没用因为新数据是“无中生有”的。你必须锁住行之间的空隙 (Gap)。2. 实战演练幻读是如何发生的假设我们有一张简单的表users有索引age。idnameage10Alice1020Bob2030Charlie30场景复现 (在 RC 隔离级别下):Session A:开启事务要把所有age 15的用户名字改成 “Senior”。UPDATEusersSETnameSeniorWHEREage15;-- 此时Bob(20) 和 Charlie(30) 的名字被改了。Session B:就在这时突然插入一个新用户 Dave年龄 25。INSERTINTOusers(id,name,age)VALUES(40,Dave,25);-- 在 Read Committed 级别下这是允许的。Session A:提交事务然后再次查询。SELECT*FROMusersWHEREage15;结果Bob (Senior)Charlie (Senior)Dave (Dave)--鬼影出现了后果Session A 明明执行了“把所有大于 15 岁的人改名”结果最后发现竟然漏了一个 Dave。这打破了事务的语义一致性。3. InnoDB 的捉鬼法器Next-Key Lock在标准 SQL 规范中只有SERIALIZABLE (串行化)级别才能解决幻读。但在 MySQL InnoDB 中REPEATABLE READ (可重复读)级别就已经解决了幻读。它是怎么做到的答案是Next-Key Lock。锁的进化史Record Lock (记录锁):只锁住单个记录。例如锁住age20这行。缺陷挡不住别人往age21插数据。Gap Lock (间隙锁):只锁住两个记录之间的空隙开区间不锁记录本身。例如锁住(10, 20)这个区间。作用专门用来防插入Next-Key Lock (临键锁):Record Lock Gap Lock的组合。锁住记录本身同时锁住它前面的空隙左开右闭区间。例如锁住(10, 20]。捉鬼实战 (在 RR 隔离级别下):回到上面的例子当 Session A 执行UPDATE ... WHERE age 15时InnoDB 不仅锁住了现有的行还布下了天罗地网锁住 (10, 20]: 保护 Bob。锁住 (20, 30]: 保护 Charlie。锁住 (30, ∞): 保护无穷大的范围。此时Session B 想要插入age25的 Dave25 落在(20, 30]的范围内。结果Session B 被阻塞 (Blocked)直到 Session A 提交。结论Next-Key Lock 通过锁住“可能插入数据的位置”彻底扼杀了幻读。4. 特殊情况快照读 vs 当前读这里有一个面试深坑。InnoDB 在 RR 级别下解决幻读有两种方式取决于你的 SQL 写法情况 A快照读 (Snapshot Read)SQL:SELECT * FROM table WHERE ...(普通的查询)机制:MVCC (多版本并发控制)。原理:事务启动时拍了一张照片。不管别人怎么插入我只看照片里的数据。效果:彻底看不到幻读新插入的数据对我不可见。情况 B当前读 (Current Read)SQL:SELECT ... FOR UPDATE,UPDATE,DELETE,INSERT。机制:Next-Key Lock。原理:我要读取最新的数据并且要加锁。为了防止我读完后别人插入我必须把间隙锁死。效果:强制阻塞插入操作物理上杜绝幻读。5. 总结幻读 (Phantom Read)是并发事务中最高级的 Boss。现象同样条件的查询多出来了新行。原因INSERT操作改变了数据总量。解法MVCC让你“视而不见”普通查询。Next-Key Lock让你“无处可插”修改/加锁查询。