# 数据库的脏读、不可重复读和幻读分别是什么?

8 min read
Table of Contents

数据库的脏读、不可重复读和幻读分别是什么?

数据库事务隔离问题:脏读、不可重复读、幻读

【核心定义】

脏读、不可重复读和幻读是数据库事务隔离级别不足时可能出现的三种数据一致性问题,分别对应不同的事务并发冲突场景。

【关键要点】

  1. 脏读(Dirty Read):一个事务读取了另一个未提交事务修改的数据

    • 本质:读到了可能被回滚的“脏数据”
    • 场景:事务A修改数据→事务B读取→事务A回滚→事务B读到不存在的数据
  2. 不可重复读(Non-repeatable Read):同一事务内多次读取同一数据,结果不一致

    • 本质:读取期间数据被其他事务修改
    • 场景:事务A读取数据→事务B修改并提交→事务A再次读取→数据值变化
  3. 幻读(Phantom Read):同一事务内多次查询同一条件,返回的记录数不一致

    • 本质:读取期间数据被其他事务增删
    • 场景:事务A查询条件→事务B插入/删除符合条件记录并提交→事务A再次查询→记录数变化

【深度推导/细节】

问题产生的根本原因

  • 并发控制缺失:数据库系统未对事务间的读写操作进行充分隔离
  • 隔离级别选择:不同隔离级别允许不同级别的并发问题存在

隔离级别与问题对应关系

隔离级别脏读不可重复读幻读
读未提交(READ UNCOMMITTED)✓✓✓
读已提交(READ COMMITTED)✗✓✓
可重复读(REPEATABLE READ)✗✗✓(MySQL InnoDB除外)
串行化(SERIALIZABLE)✗✗✗

MySQL InnoDB的特殊实现

  • 可重复读级别下通过MVCC+Next-Key Locking基本解决幻读
  • MVCC(多版本并发控制):为每个读操作创建一致性视图
  • Next-Key Locking:锁住记录和间隙,防止其他事务插入

【关联/对比】

三种问题的本质区别

  1. 操作对象不同

    • 脏读:单条记录的未提交修改
    • 不可重复读:单条记录的已提交修改
    • 幻读:记录集合的变化(增删操作)
  2. 关注维度不同

    • 脏读:数据的有效性(是否提交)
    • 不可重复读:数据的一致性(值是否变化)
    • 幻读:数据的存在性(记录是否存在)
  3. 解决方案强度

    • 解决脏读:只需保证读已提交数据
    • 解决不可重复读:需要对读取数据加锁或使用MVCC
    • 解决幻读:需要对查询范围加间隙锁

与丢失更新(Lost Update)的区别

  • 丢失更新:两个事务同时修改同一数据,后提交的覆盖先提交的
  • 上述三种问题主要关注读操作的异常,丢失更新关注写操作冲突

『面试官追问』

常见追问问题

  1. MySQL的RR级别如何避免幻读?

    • MVCC机制:通过ReadView实现一致性非锁定读
    • Next-Key Locking:当前读时使用记录锁+间隙锁
    • 示例:SELECT ... FOR UPDATE会加Next-Key Lock防止幻读
  2. 不可重复读和幻读在实际业务中的影响?

    • 不可重复读:影响余额查询、库存检查等需要数据一致性的场景
    • 幻读:影响分页查询、统计报表等需要结果集稳定的场景
  3. 如何选择合适的隔离级别?

    • 读已提交:大多数OLTP场景的平衡选择
    • 可重复读:需要事务内数据一致的财务系统
    • 串行化:对一致性要求极高的特殊场景(性能代价大)
  4. Oracle和MySQL在隔离级别实现上的差异?

    • Oracle默认隔离级别为读已提交
    • MySQL InnoDB默认隔离级别为可重复读
    • Oracle通过UNDO段实现多版本,MySQL通过UNDO日志+ReadView

实战场景分析

-- 场景1:脏读演示(需在READ UNCOMMITTED级别)
-- 事务A:UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 事务B:SELECT balance FROM account WHERE id = 1; -- 可能读到未提交的修改
-- 事务A:ROLLBACK; -- 事务B读到的数据实际不存在
-- 场景2:不可重复读演示
-- 事务A:SELECT balance FROM account WHERE id = 1; -- 第一次读取
-- 事务B:UPDATE account SET balance = balance + 200 WHERE id = 1; COMMIT;
-- 事务A:SELECT balance FROM account WHERE id = 1; -- 第二次读取,结果不同
-- 场景3:幻读演示
-- 事务A:SELECT COUNT(*) FROM orders WHERE user_id = 1; -- 返回10条
-- 事务B:INSERT INTO orders(user_id, ...) VALUES(1, ...); COMMIT;
-- 事务A:SELECT COUNT(*) FROM orders WHERE user_id = 1; -- 返回11条

【版本差异与演进】

MySQL 5.x vs 8.0

  • 5.x:可重复读级别通过MVCC部分解决幻读
  • 8.0:优化了锁机制,增强了一致性保证

新兴数据库的处理

  • PostgreSQL:使用SSI(可串行化快照隔离)更优雅地解决幻读
  • 分布式数据库:通常采用更严格的隔离级别或乐观锁机制

最佳实践建议

  1. 默认使用数据库的默认隔离级别(MySQL RR,Oracle RC)
  2. 在事务中尽早锁定需要的数据,减少持有锁的时间
  3. 合理设计事务边界,避免长事务
  4. 对于写后读场景,使用SELECT … FOR UPDATE明确加锁
  5. 考虑使用乐观锁(版本号)替代悲观锁,提高并发性能

总结:理解脏读、不可重复读和幻读的关键在于把握它们分别对应”未提交数据”、“数据修改”和”数据增减”这三种不同的并发冲突场景。在实际开发中,应根据业务需求选择合适的隔离级别,并在性能和数据一致性之间找到平衡点。MySQL在可重复读级别通过MVCC和Next-Key Locking的巧妙设计,为开发者提供了高性能且高一致性的解决方案。

My avatar

文章看完了,不打算去别的篇章转转吗?👀 感谢你的耐心阅读!如果你有问题想探讨,或者发现了文中的 Bug,请务必通过页脚的链接联系我


More Posts

# 说说 AQS 吧?

6 min read

AQS 是 Java 并发包中构建锁和同步器的核心框架,通过一个 FIFO 队列 管理线程的排队与唤醒,并利用一个 volatile int 型 state 变量 表示同步状态。

Read

Comments