Appearance
脏读、幻读、不可重复读区别
一句话答案
三个都是"一个事务读到了不该读的数据"。按严重程度从高到低排序:
- 脏读:读到别人没提交的。
- 不可重复读:两次读同一行结果变了,别人 update 提交了。
- 幻读:两次按同一条件查询,行数变了,别人 insert/delete 提交了。
后两者读到的都是已提交数据,属于"读的时机"问题。脏读读到的是随时可能回滚的数据,危害最大。
不可重复读和脏读的区别是:脏读是某一事务读取了另一个事务未提交的脏数据。而不可重复读则是读取了前一事务提交的数据。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同)。所不同的是不可重复读查询的都是同一个数据项。幻读针对的是一批数据整体(比如数据的个数)。
不可重复读和幻读到底有什么区别呢?
- 不可重复读是读取了其他事务更改的数据,针对update操作
解决:使用行级锁,锁定该行。事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
- 幻读是读取了其他事务新增的数据,针对insert和delete操作
解决:使用表级锁,锁定整张表。事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
一、三个现象逐个场景推演
1、脏读(Dirty Read):读到了未提交的数据
| 时刻 | 事务 A | 事务 B |
|---|---|---|
| t1 | 开始事务,查余额 = 100 | |
| t2 | 开始事务,UPDATE 余额 = 200(未提交) | |
| t3 | 再查余额 = 200(读到脏数据) | |
| t4 | ROLLBACK,余额回到 100 | |
| t5 | 基于 200 做业务决策 | —— 数据是错的 |
要点:A 读到的 200 是一个从未存在过的中间状态。B 一旦回滚,A 拿着幻觉数据继续跑,转账、扣减类业务直接资损。
2、不可重复读(Non-Repeatable Read):同一行两次读不一样
| 时刻 | 事务 A | 事务 B |
|---|---|---|
| t1 | 开始事务,查余额 = 100 | |
| t2 | UPDATE 余额 = 200 并 COMMIT | |
| t3 | 再查同一行,余额 = 200 |
要点:两次读同一行,值变了——针对 update,破坏的是"一个事务内多次读取应一致"的预期。
3、幻读(Phantom Read):同一条件两次查,行数变了
| 时刻 | 事务 A | 事务 B |
|---|---|---|
| t1 | 开始事务,SELECT COUNT(*) = 10 | |
| t2 | INSERT 一条新记录并 COMMIT | |
| t3 | 同条件再 COUNT(*) = 11(多出"幻影"行) |
要点:两次读的是一批数据,多/少了行——针对 insert/delete,破坏的是"同一条件查询结果集应稳定"的预期。个别行更新并不改变行数,所以它和不可重复读关注点不同。
二、隔离级别对照矩阵
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| READ UNCOMMITTED 读未提交 | 可能 | 可能 | 可能 | 性能最高,几乎不用 |
| READ COMMITTED 读已提交(Oracle/PG 默认) | 不可能 | 可能 | 可能 | 只能读到已提交数据,语句级快照 |
| REPEATABLE READ 可重复读(MySQL InnoDB 默认) | 不可能 | 不可能 | 不可能(个别场景除外) | MVCC + 临键锁双管齐下 |
| SERIALIZABLE 串行化 | 不可能 | 不可能 | 不可能 | 读加共享锁,完全串行,并发最差 |
记忆口诀:级别每升一级,依次消灭脏读 → 不可重复读 → 幻读;代价是并发度逐级下降。
三、MVCC 如何"顺手"消除前两个
MVCC(多版本并发控制)不做全局加锁,而是给每行藏版本、给每个事务发快照,让读写互不阻塞:
版本链:InnoDB 每行有两个隐藏列——DB_TRX_ID(最近修改它的事务 id)和 DB_ROLL_PTR(指向 undo log 里的旧版本)。一行数据的历次修改串成一条链;
ReadView(快照):事务发起快照读时记录当时"活跃(未提交)事务集合"。读某行时沿版本链回溯,找到对自己可见的第一个版本。未提交的修改对当前事务不可见,脏读天然被消除;
RC vs RR 的差别只在快照时机:RC 每条语句生成一个新 ReadView(所以能看见别人新提交的 update → 不可重复读)。RR 只在事务第一次读时生成并复用到事务结束(前后一致 → 可重复读);
幻读的消除要靠两种手段:快照读(普通 SELECT)靠 RR 级别的固定 ReadView 看不见新插入的行。当前读(SELECT ... FOR UPDATE / UPDATE / DELETE)靠临键锁(Next-Key Lock = 记录锁 + 间隙锁)锁住区间,堵住 insert 的入口。所以原文"解决幻读要用表级锁"是简化说法——InnoDB 用更细粒度的临键锁达到了同样效果;
遗留边界:RR 下先快照读再当前读、或本事务自己 update 之后再看,仍可能看见幻影行。严格场景要显式加锁或升到 SERIALIZABLE。
延伸阅读:数据库两大神器之锁(锁粒度与 MVCC 的关系)、数据库两大神器之索引、[Spring 事务面试考点](../../Spring/04-事务与升级进阶/Spring 事务面试考点.md)。
2026-10 扩充(Round1 查漏)。