脏读、幻读、不可重复读是什么?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道三个概念的定义,更是想知道你是否理解它们产生的根本原因 —— 并发事务之间的数据干扰,以及它们之间的本质区别。
  2. 隔离级别理解:考察你是否清楚 MySQL 的四种事务隔离级别,以及每种隔离级别能解决哪些并发问题。
  3. 实践应用能力:能否根据业务场景选择合适的隔离级别,是否了解 MVCC(多版本并发控制)如何解决这些问题。

核心答案

脏读、不可重复读、幻读是 数据库并发事务 中三种典型的数据不一致问题:

问题 定义 关注点 严重程度
脏读 读到其他事务未提交的数据 数据被回滚 ⭐⭐⭐ 最严重
不可重复读 同一事务内,两次读取同一行数据结果不同 数据被修改/删除 ⭐⭐ 中等
幻读 同一事务内,两次查询结果行数不同 数据被新增 ⭐ 较轻

一句话总结:脏读是读了未提交的数据;不可重复读是同一条数据变了;幻读是结果集行数变了。

深度解析

一、三种问题详解

时间线 事务 A 事务 B
T1 BEGIN;
T2 BEGIN;
T3 UPDATE users SET age=25 WHERE id=1;(未提交)
T4 SELECT age FROM users WHERE id=1; → 读到 age=25(未提交!)
T5 ROLLBACK;(回滚了!)
T6 我读到的数据是脏数据…

问题:事务 A 读到了事务 B 未提交的数据,事务 B 回滚后,事务 A 拿到的数据就不合法了。

上图展示了脏读的发生过程:

  • 时间点 T3:事务 B 将 id=1 的 age 从 20 改为 25,但尚未提交
  • 时间点 T4:事务 A 读取该行数据,得到 age=25
  • 时间点 T5:事务 B 回滚了,数据恢复为 20
  • 问题:事务 A 读到的 age=25 是 “脏数据”,因为事务 B 根本没提交
    脏读危害:基于未提交数据做业务判断,可能导致严重的逻辑错误。
时间线 事务 A 事务 B
T1 BEGIN;
T2 SELECT age FROM users WHERE id=1; → 读到 age=20
T3 BEGIN;
T4 UPDATE users SET age=25 WHERE id=1;
T5 COMMIT;
T6 SELECT age FROM users WHERE id=1; → 读到 age=25(变了!)
T7 同一事务两次读取,结果不一致!

问题:事务 A 内两次读取同一行数据结果不同,因为事务 B 在中间修改并提交了这条数据。

上图展示了不可重复读的发生过程:

  • 时间点 T2:事务 A 第一次读取,age=20
  • 时间点 T4~T5:事务 B 修改了这条数据并提交
  • 时间点 T6:事务 A 再次读取同一行,age=25
  • 问题:同一事务内,两次读取同一数据,结果不一致
    与脏读的区别:不可重复读读到的是已提交的数据,不是脏数据。
时间线 事务 A 事务 B
T1 BEGIN;
T2 SELECT * FROM users WHERE age > 20; → 返回 2 条记录
T3 BEGIN;
T4 INSERT INTO users(age) VALUES(30);
T5 COMMIT;
T6 SELECT * FROM users WHERE age > 20; → 返回 3 条记录!
T7 同一条件查询,结果行数变了!

问题:事务 A 内两次相同条件查询结果行数不同,因为事务 B 在中间新增了一条符合条件的数据。

上图展示了幻读的发生过程:

  • 时间点 T2:事务 A 查询 age > 20 的记录,返回 2 条

  • 时间点 T4~T5:事务 B 新增了一条 age=30 的记录并提交

  • 时间点 T6:事务 A 再次用相同条件查询,返回 3 条

  • 问题:同一事务内,两次相同查询,结果行数不同,仿佛出现了 “幻影”
    与不可重复读的区别:

  • 不可重复读强调 同一条数据被修改(UPDATE/DELETE)

  • 幻读强调 结果集行数变化(INSERT/DELETE)

二、四种隔离级别

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED(读未提交) 可能发生 可能发生 可能发生
READ COMMITTED(读已提交) 已解决 可能发生 可能发生
REPEATABLE READ(可重复读) 已解决 已解决 已解决
SERIALIZABLE(串行化) 已解决 已解决 已解决

注:MySQL InnoDB 默认 REPEATABLE READ,通过 MVCC + Next-Key Lock 在很大程度上解决了幻读问题。

隔离级别详解:

  1. READ UNCOMMITTED(读未提交)
    • 最低隔离级别
    • 可能发生脏读、不可重复读、幻读
    • 几乎不使用
  2. READ COMMITTED(读已提交,RC)
    • 只能读取已提交的数据
    • 解决了脏读,但可能发生不可重复读、幻读
    • Oracle、SQL Server 默认级别
  3. REPEATABLE READ(可重复读,RR)
    • MySQL InnoDB 默认隔离级别
    • 保证同一事务内多次读取同一数据结果一致
    • 解决了脏读、不可重复读
    • InnoDB 通过 MVCC + Next-Key Lock 也解决了幻读
  4. SERIALIZABLE(串行化)
    • 最高隔离级别
    • 强制事务串行执行
    • 解决所有问题,但性能最差

三、MySQL 如何解决这些问题

机制 要点
MVCC(多版本并发控制) 每行数据保存 DB_TRX_ID(最后修改的事务 ID);读操作根据 Read View 判断可见性;读不阻塞写,写不阻塞读;RC 级别每次 SELECT 生成新的 Read View,RR 级别只在第一次 SELECT 生成 Read View
锁机制 共享锁(S 锁)用于读操作,排他锁(X 锁)用于写操作;行锁锁定单行记录;间隙锁(Gap Lock)锁定一个范围;Next-Key Lock = 行锁 + 间隙锁
RR 级别解决幻读 快照读(普通 SELECT)通过 MVCC 解决;当前读(SELECT … FOR UPDATE)通过 Next-Key Lock 锁住范围,防止新增

MVCC 解决不可重复读:

-- RR 级别下,MVCC 保证可重复读
-- 事务 A
BEGIN;
SELECT * FROM users WHERE id = 1; -- age=20,生成 Read View

-- 此时事务 B 修改并提交
-- UPDATE users SET age=25 WHERE id=1; COMMIT;

SELECT * FROM users WHERE id = 1; -- 仍然读到 age=20
-- 因为 MVCC 根据事务 A 的 Read View,只能看到历史版本
COMMIT;

Next-Key Lock 解决幻读:

-- RR 级别下,当前读使用 Next-Key Lock
-- 事务 A
BEGIN;
SELECT * FROM users WHERE age > 20 FOR UPDATE;
-- 锁定 age > 20 的所有行,以及 (20, +∞) 的间隙

-- 此时事务 B 尝试插入
-- INSERT INTO users(age) VALUES(30); -- 被阻塞!

COMMIT; -- 事务 A 提交后,事务 B 才能插入

四、对比总结

对比项 脏读 不可重复读 幻读
读取的数据 未提交数据 已提交数据 已提交数据
关注点 内容是否有效 内容是否一致 行数是否变化
涉及操作 UPDATE / DELETE UPDATE / DELETE INSERT / DELETE
严重程度 最严重 中等 较轻
解决方案 读已提交(RC) 可重复读(RR) 串行化,或 RR + Next-Key Lock

面试高频追问

  1. MySQL 默认隔离级别是什么?如何解决幻读?
    • 默认是 REPEATABLE READ(可重复读)
    • 通过 MVCC 解决快照读的幻读
    • 通过 Next-Key Lock(行锁 + 间隙锁)解决当前读的幻读
  2. RC 和 RR 的区别?
    • RC:每次 SELECT 生成新的 Read View,能看到其他事务已提交的修改
    • RR:第一次 SELECT 生成 Read View,后续复用,保证可重复读
  3. 什么是 MVCC?
    • 多版本并发控制,每行数据保存版本链(通过 undo log)
    • 读操作根据 Read View 判断哪个版本对自己可见
    • 实现 “读不阻塞写,写不阻塞读”
  4. 什么时候用 SERIALIZABLE?
    • 需要严格数据一致性的场景(如金融交易)
    • 一般不推荐使用,性能太差

常见面试变体

  • “MySQL 有哪些事务隔离级别?”
  • “RR 级别能完全解决幻读吗?”
  • “MVCC 是怎么实现的?”
  • “什么是 Next-Key Lock?”

记忆口诀

脏读未提交,数据不可信; 不可重复读,同条数据变; 幻读多或少,结果行数变。 RC 解脏读,RR 解后俩,串行全解决。

总结

脏读是读到未提交的数据(最严重);不可重复读是同一事务内同一行数据变了(UPDATE/DELETE);幻读是同一事务内查询结果行数变了(INSERT/DELETE)。MySQL 默认使用 REPEATABLE READ 隔离级别,通过 MVCC 解决不可重复读,通过 MVCC + Next-Key Lock 解决幻读。


声明:本文转载自小哈的博客 《脏读、幻读、不可重复读是什么?》,版权归原文作者所有。转载请保留本声明与原文链接。