MYSQL RR 解决“脏读+不可重复读“和“幻读“的本质区别
RR 解决脏读 + 不可重复读 和 RR 解决幻读:MVCC 解决快照读。这两个是否矛盾
一、先说结论:完全不矛盾
| 问题 | 解决机制 | 解决哪类读 |
|---|---|---|
| 脏读 + 不可重复读 | MVCC(ReadView 复用) | 单行读 |
| 幻读 | MVCC(快照读) | 范围读 |
| 幻读(当前读) | Next-Key Lock | 范围读 + 锁 |
关键洞察:
- MVCC 一个机制,既解决了不可重复读,又解决了快照读的幻读
- 两种场景都是"读快照",RR 下 ReadView 复用 → 看不到其他事务的新数据
- 不可重复读和幻读本质相同:都是"读不到其他事务新数据"——单行级别就是不可重复读,范围级别就是幻读
二、3 大问题本质
不可重复读:单行被修改
-- 老哥的报表例子 BEGIN; SELECT balance FROM account WHERE id = 1; -- 第 1 次读,balance=1000 -- 其他事务:UPDATE account SET balance = 900 WHERE id = 1; COMMIT; SELECT balance FROM account WHERE id = 1; -- 第 2 次读,balance=900 -- ⚠️ 同一事务,同一行,结果不同 → 不可重复读幻读:范围被插入
-- 老哥的批量报表例子 BEGIN; SELECT * FROM account WHERE balance > 1000; -- 第 1 次查,5 条 -- 其他事务:INSERT INTO account (balance) VALUES (2000); COMMIT; SELECT * FROM account WHERE balance > 1000; -- 第 2 次查,6 条 -- ⚠️ 同一事务,同一范围,行数不同 → 幻读本质对比:
| 维度 | 不可重复读 | 幻读 |
|---|---|---|
| 关注点 | 单行被修改 | 范围被插入/删除 |
| 结果变化 | 值变了 | 行数变了 |
| 底层机制 | 都是 MVCC ReadView 复用 | 都是 MVCC ReadView 复用 |
| 区别 | 同一行的不同版本 | 范围中多了/少了行 |
**所以——MVCC 解决不可重复读,自然就解决了快照读的幻读。因为它们都是"看不到新数据"。
三、MVCC 一个机制,两个效果
RR 隔离级别下: - 事务开始第一次 SELECT 时创建 ReadView - 整个事务期间复用这个 ReadView - 看不到 ReadView 之后才提交的数据 ↓ 看不到"被修改的数据"(不可重复读解决) ↓ 看不到"被插入的数据"(幻读解决)一句话总结:
"MVCC 看不到 ReadView 之后的新数据,自然就既解决不可重复读(修改)又解决幻读(插入)。一个机制,两个效果。"
四、为什么会有"矛盾"的感觉?
感觉矛盾,可能是因为4 大隔离级别的标准定义:
| 隔离级别 | 解决 | 没解决 |
|---|---|---|
| RU | 无 | 脏读 / 不可重复读 / 幻读 |
| RC | 脏读 | 不可重复读 / 幻读 |
| RR(标准) | 脏读 /不可重复读 | 幻读 |
| SE | 全部 | 无 |
标准 SQL 定义里 RR 是不解决幻读的!但MySQL InnoDB 通过 MVCC + Next-Key Lock 突破了标准定义,几乎解决了幻读。
所以老哥看到的两个说法:
1."RR 解决脏读 + 不可重复读"(标准 SQL 定义)
2."RR 解决幻读"(MySQL InnoDB 实际实现)
它们都对!只是描述的角度不同:
- 角度 1:按标准 SQL 定义,RR 不解决幻读
- 角度 2:按 MySQL InnoDB 实现,RR通过 MVCC + Next-Key Lock 几乎解决幻读
五、RR 解决幻读的 2 大机制(MVCC 解决快照读 + Next-Key Lock 解决当前读)
机制 1:MVCC 解决快照读的幻读(普通 SELECT)
-- RR 隔离级别 + 普通 SELECT BEGIN; -- 创建 ReadView,假设 m_ids=[2,3,4,5], min=2, max=6 SELECT * FROM account WHERE balance > 1000; -- 看到 5 条 -- 期间事务 6 INSERT 并提交一条 -- 事务 6 的 trx_id=6 > max=6,不在 ReadView 范围内 SELECT * FROM account WHERE balance > 1000; -- 仍看到 5 条(ReadView 复用) COMMIT;关键:ReadView 看不到 trx_id > max_trx_id 的事务提交的数据,所以新插入的行看不到,幻读解决。
机制 2:Next-Key Lock 解决当前读的幻读(SELECT FOR UPDATE)
-- RR 隔离级别 + 当前读 BEGIN; SELECT * FROM account WHERE balance > 1000 FOR UPDATE; -- 加 Next-Key Lock -- 锁定范围:balance > 1000 涉及的索引区间 -- 期间其他事务尝试 INSERT balance > 1000 INSERT INTO account (balance) VALUES (2000); -- ⚠️ 阻塞! SELECT * FROM account WHERE balance > 1000; -- 仍看到 5 条 COMMIT;关键:Next-Key Lock = 记录锁 + 间隙锁,锁定了"可能插入的位置",防止新数据插入。
六、面试话术
"不矛盾。
'RR 解决脏读 + 不可重复读'是标准 SQL 定义——按 SQL 标准 RR 不解决幻读。
'RR 解决幻读'是MySQL InnoDB 实际实现——InnoDB 用MVCC 解决快照读幻读(ReadView 复用),用Next-Key Lock 解决当前读幻读(记录锁+间隙锁)。
核心洞察:不可重复读和幻读本质相同——都是看不到其他事务的新数据。单行级别叫不可重复读,范围级别叫幻读。MVCC 一个机制同时解决。"
七、项目实战对照
RR 默认
@Transactional // RR public void generateReport(Report report) { // 1. 单行查(不可重复读解决) Report existing = reportMapper.selectById(report.getId()); // 整个事务期间,existing 不会被其他事务的修改影响 // 2. 范围查(幻读解决) List<Report> pending = reportMapper.selectByStatus("pending"); // 整个事务期间,pending 不会被其他事务的插入影响 // 3. 当前读(Next-Key Lock 解决幻读) List<Report> all = reportMapper.selectByStatusForUpdate("pending"); // 加锁,其他事务不能 INSERT status='pending' 的报表 }RC 查询
@Transactional(isolation = Isolation.READ_COMMITTED) // RC public List<MaskedData> queryLatestMasked() { // 1. 单行查(能重复读 → 老数据)— mpvs 不在意 // 2. 范围查(能幻读 → 新数据)— mpvs 在意,要看最新 return dataMaskMapper.selectAll(); // 看到最新 }用 RR:不可重复读 + 幻读都不能有(同一报表要一致)。
用 RC:能看到最新(有些任务要看最新数据)。"
八、记忆口诀
"不可重复读和幻读,本质都是看不到新数据"
"单行级别 = 不可重复读,范围级别 = 幻读"
"MVCC 一个机制,同时解决两个"
"标准 SQL RR 不解决幻读,MySQL InnoDB 解决了"
"快照读靠 MVCC,当前读靠 Next-Key Lock"
