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

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/

面试考察点

面试官提出这个问题,通常意在考察以下几个方面:

  1. 对数据库事务核心特性的理解:事务的 ACID 特性是基础,而隔离性(Isolation)直接衍生出这些问题。
  2. 对并发事务可能引发问题的洞察力:不仅仅是背诵定义,更是想知道你是否能理解在多个事务并发执行时,数据一致性可能受到何种挑战。
  3. 对 MySQL 隔离级别机制的掌握:考察你是否清楚,数据库通过设置不同的隔离级别(Read Uncommitted, Read Committed, Repeatable Read, Serializable)来 trade-off 性能与一致性,从而解决这些现象。
  4. 解决实际问题的能力:在真实的高并发业务场景(如金融交易、库存扣减)中,你是否能识别并选择正确的隔离级别或加锁策略来规避这些问题。

核心答案

脏读、幻读和不可重复读是数据库事务隔离性被破坏时,可能出现的三种典型问题。它们都源于并发事务之间的相互干扰。

  • 脏读:一个事务读到了另一个未提交事务修改过的数据。如果那个事务回滚,则读到的数据就是无效的“脏数据”。
  • 不可重复读:在同一个事务内,两次相同的查询读到了不同的数据行内容。这通常是因为在两次读取之间,另一个事务修改并提交了这些数据。
  • 幻读:在同一个事务内,两次相同的范围查询读到了不同的数据行集合(即出现了新的“幻影”行或原有行消失)。这通常是因为在两次查询之间,另一个事务插入或删除了符合查询条件的数据行并提交。

它们与隔离级别的对应关系是:

  • 读未提交 级别下,所有问题都可能发生。
  • 读已提交 级别下,解决了脏读
  • 可重复读(MySQL InnoDB 引擎默认级别)下,解决了脏读不可重复读,并通过Next-Key Lock 锁很大程度上避免了幻读
  • 串行化 级别下,通过强制事务串行执行,解决了所有问题,但性能代价最高。

深度解析

原理与场景化解释

这三种现象的本质区别在于其他事务进行的是 “写” 操作还是 “增删” 操作,以及本事务读取时,其他事务是否已提交。

  1. 脏读

    • 场景:事务 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
      
  2. 不可重复读 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 UPDATEUPDATEDELETE 语句),REPEATABLE READ 级别下 InnoDB 使用 Next-Key Lock(记录锁 + 间隙锁)来锁住记录及其附近的间隙,从而阻止其他事务在间隙中插入数据,很大程度上避免了幻读。这是 MySQL 相较于标准 SQL 的增强。
  • 最佳实践
    1. 理解默认的 REPEATABLE READ 级别在大多数场景下已足够安全,无需盲目提升到 SERIALIZABLE
    2. 在需要绝对防止幻读的关键业务(如账户资金变动、唯一性校验),应在事务中使用显式加锁(如 SELECT ... FOR UPDATE),并确保所有相关查询都走索引,以便 Next-Key Lock 能精确锁定范围,避免锁表。
    3. 在设计阶段,评估业务对一致性的要求。例如,一个后台统计报表可能允许不可重复读(使用 READ COMMITTED 以获得更高并发),而一个转账操作则必须保证可重复读甚至更严格。

常见误区

  • 误区一:认为“可重复读”级别完全解决了幻读。实际上,它解决了快照读的幻读,但对于当前读,是通过 Next-Key Lock 预防,而非从原理上根除。在某些极端复杂的交互序列下,仍然可能观察到类似幻读的现象(但极少见)。
  • 误区二:将“不可重复读”和“幻读”混为一谈。记住关键:“值变”是不可重复读,“行数变”是幻读

总结

简单来说,脏读是读了未提交的“垃圾”数据,不可重复读是两次读同一行数据内容变了,而幻读是两次读同一条件数据集合的行数变了。理解它们的区别和成因,是正确选用数据库隔离级别、设计高并发下数据安全访问方案的基础。