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"