MySQL InnoDB 的可重复读通过 MVCC(快照读)和 Next-Key Lock(当前读)组合机制几乎消除幻读,但非绝对:快照读复用同一版本视图,当前读加间隙锁阻塞插入;例外如无索引查询、唯一索引等值查询不加间隙锁、“快照读后写入”逻辑漏洞等仍可能导致幻读。

MySQL 的可重复读(Repeatable Read)隔离级别在 Java 应用中并非靠 Java 本身实现防幻读,而是依赖底层 InnoDB 存储引擎的机制。Java(如 JDBC、Spring)只是将事务隔离级别设置传递给数据库,并配合其行为做正确使用。真正防止幻读的是 MySQL InnoDB 的 MVCC + Next-Key Lock(临键锁)组合策略,但需注意:它“几乎防止”而非“绝对杜绝”幻读。
下面从 Java 开发者的实际视角讲清楚关键点:
Java 中如何让可重复读真正起到防幻读作用
设置正确的事务隔离级别
在 Spring 中通过@Transactional(isolation = Isolation.REPEATABLE_READ)显式声明;
或在 JDBC 连接中执行connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ)。
⚠️ 注意:MySQL 默认就是 RR,但若连接池或框架覆盖了该值(如某些 HikariCP 配置),仍需确认生效。-
区分快照读与当前读——这是防幻读成败的关键
- 快照读(普通 SELECT):InnoDB 自动使用 MVCC,复用事务开启时的 ReadView,后续查询看到的始终是同一份数据快照,自然不会出现新插入的行 → 从读的角度屏蔽幻读。
- 当前读(SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT ... SELECT):触发加锁,InnoDB 使用 Next-Key Lock(行锁 + 间隙锁) 锁定查询范围,阻止其他事务在该范围内插入新记录 → 从写的维度阻断幻读源头。
-
避免“快照读后写入”陷阱(典型幻读残留场景)
比如:// 事务内第一次查:SELECT * FROM user WHERE age > 20; → 返回 5 行 // 其他事务插入 age=25 的用户并提交 // 事务内再执行:INSERT INTO user VALUES (..., 25, ...); ——没加锁、不走当前读!
此时插入成功,后续再查可能多出一行。这不是隔离级别失效,而是业务逻辑未对写操作施加足够约束。
✅ 正确做法:需要一致性写入时,应先用SELECT ... FOR UPDATE锁定范围,再执行 INSERT/UPDATE。 索引类型影响间隙锁是否生效
若查询条件中的字段没有索引,或仅是非唯一普通索引的等值查询,InnoDB 可能退化为全表扫描+锁表,或不加间隙锁,导致幻读风险上升。
✅ 建议:确保 WHERE 条件字段有合适索引(最好是唯一索引或联合索引覆盖查询范围)。
为什么有人说“RR 不能完全解决幻读”
因为标准 SQL 定义的“幻读”包含所有满足条件的新行插入,而 InnoDB 的防护有边界:
立即学习“Java免费学习笔记(深入)”;
- 对无索引字段的范围查询,间隙锁可能无法精确锁定;
- 对唯一索引的等值查询(如 WHERE id = ?),InnoDB 优化为只加行锁,不加间隙锁,此时并发插入相同唯一值会报错,但插入其他值仍可能“漏进”;
- 显式快照读后,再执行不带锁的 INSERT,属于应用层逻辑漏洞,不在数据库隔离机制保障范围内。
本质上,MySQL 的 RR 是工程级强一致方案,不是理论级串行化,它在性能与安全间做了务实取舍。
不复杂但容易忽略


















