MySQL 脏读、幻读、不可重复读是什么?
2025年12月28日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,通常意在考察以下几个方面:
- 对数据库事务核心特性的理解:事务的 ACID 特性是基础,而隔离性(Isolation)直接衍生出这些问题。
- 对并发事务可能引发问题的洞察力:不仅仅是背诵定义,更是想知道你是否能理解在多个事务并发执行时,数据一致性可能受到何种挑战。
- 对 MySQL 隔离级别机制的掌握:考察你是否清楚,数据库通过设置不同的隔离级别(Read Uncommitted, Read Committed, Repeatable Read, Serializable)来 trade-off 性能与一致性,从而解决这些现象。
- 解决实际问题的能力:在真实的高并发业务场景(如金融交易、库存扣减)中,你是否能识别并选择正确的隔离级别或加锁策略来规避这些问题。
核心答案
脏读、幻读和不可重复读是数据库事务隔离性被破坏时,可能出现的三种典型问题。它们都源于并发事务之间的相互干扰。
- 脏读:一个事务读到了另一个未提交事务修改过的数据。如果那个事务回滚,则读到的数据就是无效的“脏数据”。
- 不可重复读:在同一个事务内,两次相同的查询读到了不同的数据行内容。这通常是因为在两次读取之间,另一个事务修改并提交了这些数据。
- 幻读:在同一个事务内,两次相同的范围查询读到了不同的数据行集合(即出现了新的“幻影”行或原有行消失)。这通常是因为在两次查询之间,另一个事务插入或删除了符合查询条件的数据行并提交。
它们与隔离级别的对应关系是:
- 读未提交 级别下,所有问题都可能发生。
- 读已提交 级别下,解决了脏读。
- 可重复读(MySQL InnoDB 引擎默认级别)下,解决了脏读和不可重复读,并通过Next-Key Lock 锁很大程度上避免了幻读。
- 串行化 级别下,通过强制事务串行执行,解决了所有问题,但性能代价最高。
深度解析
原理与场景化解释
这三种现象的本质区别在于其他事务进行的是 “写” 操作还是 “增删” 操作,以及本事务读取时,其他事务是否已提交。
-
脏读
-
场景:事务 A 将一条记录的余额从 100 修改为 200(未提交)。此时事务 B 读取这条记录,得到余额 200。随后事务 A 因故回滚,余额恢复为 100。那么事务 B 读到的 200 就是从未真实存在过的“脏数据”。
-
代码示例(伪时序):
事务A:BEGIN; -- ① 事务A:UPDATE account SET balance = 200 WHERE id = 1; -- ② 未提交! 事务B:BEGIN; -- ③ -- 在读未提交级别下,可能读到200这个中间状态 事务B:SELECT balance FROM account WHERE id = 1; -- ④ 读到了200(脏读) 事务A:ROLLBACK; -- ⑤ 数据回滚到100
-
-
不可重复读 vs 幻读
- 核心区别:不可重复读针对的是已存在数据行的值被修改;幻读针对的是数据行集合的数量因插入或删除而发生变化。
- 场景对比:
- 不可重复读:事务 A 第一次查询 id=1 的余额为 100。此时事务 B 将余额更新为 150 并提交。事务 A 再次查询 id=1 的余额,得到了 150。同一行,值变了。
- 幻读:事务 A 第一次查询
age > 20的用户,得到 10 条记录。此时事务 B 插入了一个 age=25 的新用户并提交。事务 A 再次以相同条件查询,得到了 11 条记录。多出了一行“幻影”。即使事务 B 是删除了 1 条记录,导致事务 A 第二次查询只得到 9 条记录,这也属于幻读。
MySQL InnoDB 的解决方案与最佳实践
- 可重复读与幻读:这是最易混淆的点。MySQL InnoDB 在
REPEATABLE READ级别下,通过MVCC解决了快照读(普通SELECT语句)的不可重复读和幻读问题。事务第一次读取时会建立一个一致性读视图,后续读取都基于这个视图,看不到其他事务提交的修改。 - 但是,对于当前读(如
SELECT ... FOR UPDATE、UPDATE、DELETE语句),REPEATABLE READ级别下 InnoDB 使用Next-Key Lock(记录锁 + 间隙锁)来锁住记录及其附近的间隙,从而阻止其他事务在间隙中插入数据,很大程度上避免了幻读。这是 MySQL 相较于标准 SQL 的增强。 - 最佳实践:
- 理解默认的
REPEATABLE READ级别在大多数场景下已足够安全,无需盲目提升到SERIALIZABLE。 - 在需要绝对防止幻读的关键业务(如账户资金变动、唯一性校验),应在事务中使用显式加锁(如
SELECT ... FOR UPDATE),并确保所有相关查询都走索引,以便 Next-Key Lock 能精确锁定范围,避免锁表。 - 在设计阶段,评估业务对一致性的要求。例如,一个后台统计报表可能允许不可重复读(使用
READ COMMITTED以获得更高并发),而一个转账操作则必须保证可重复读甚至更严格。
- 理解默认的
常见误区
- 误区一:认为“可重复读”级别完全解决了幻读。实际上,它解决了快照读的幻读,但对于当前读,是通过 Next-Key Lock 预防,而非从原理上根除。在某些极端复杂的交互序列下,仍然可能观察到类似幻读的现象(但极少见)。
- 误区二:将“不可重复读”和“幻读”混为一谈。记住关键:“值变”是不可重复读,“行数变”是幻读。
总结
简单来说,脏读是读了未提交的“垃圾”数据,不可重复读是两次读同一行数据内容变了,而幻读是两次读同一条件数据集合的行数变了。理解它们的区别和成因,是正确选用数据库隔离级别、设计高并发下数据安全访问方案的基础。