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

#中间件#面试八股

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

结论:三者都是并发事务互相“看见”对方未完成或已提交的修改所导致的现象。脏读是读到了别人未提交的数据;不可重复读是同一行两次读到的值不同;幻读是同一条件两次读到的行数不同。隔离级别逐级加严,MySQL 默认的可重复读靠 MVCC 快照 + 间隙锁来压制它们。

一、三种现象对照

现象读到了什么触发动作后果被哪个级别消除
脏读 Dirty Read其他事务未提交的修改UPDATE 未提交数据可能被回滚,读到不存在的值读已提交及以上
不可重复读 Non-Repeatable Read其他事务已提交的 UPDATE/DELETE同一行被改同一事务内两次读不一致可重复读及以上
幻读 Phantom Read其他事务已提交的 INSERT范围查询行数变化第二次多出或少了行串行化;可重复读下部分场景仍会出现

二、并发时序

脏读
  T1: BEGIN; UPDATE account SET bal = 0 WHERE id = 1;   -- 未提交
  T2: SELECT bal FROM account WHERE id = 1;             -- 读到 0
  T1: ROLLBACK;                                          -- T2 读的是幻觉

不可重复读
  T1: BEGIN; SELECT bal FROM account WHERE id = 1;      -- 100
  T2: UPDATE account SET bal = 50 WHERE id = 1; COMMIT;
  T1: SELECT bal FROM account WHERE id = 1;             -- 50,同一事务内变了

幻读
  T1: BEGIN; SELECT COUNT(*) FROM t_order WHERE uid = 7;  -- 3
  T2: INSERT INTO t_order (uid) VALUES (7); COMMIT;
  T1: SELECT COUNT(*) FROM t_order WHERE uid = 7;         -- 4,多出一行

三、动手复现

开两个会话,把隔离级别设成读未提交,脏读立刻可见:

-- 会话 A
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
UPDATE account SET balance = 0 WHERE id = 1;
-- 先不提交

-- 会话 B
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT balance FROM account WHERE id = 1;   -- 读到 0(脏读)

-- 回到 A
ROLLBACK;

可重复读下同一行不会变,但当前读仍可能看到新行:

-- 会话 A(RR)
BEGIN;
SELECT COUNT(*) FROM t_order WHERE uid = 7;      -- 快照读,3
-- 会话 B 插入一行并提交

SELECT COUNT(*) FROM t_order WHERE uid = 7;      -- 仍是 3(MVCC 快照)
SELECT COUNT(*) FROM t_order WHERE uid = 7 FOR UPDATE;  -- 4(当前读,幻读出现)

在 RR 下,SELECT ... FOR UPDATE、UPDATE、DELETE 都是当前读,会看最新数据;InnoDB 用 next-key lock(记录锁 + 间隙锁)阻止其他事务在范围内插入,从而让「当前读 + 后续写」不出现新行。但如果两次快照读之间自己执行了当前读或写入了新行,幻读仍会发生。

四、隔离级别与实现手段

级别脏读不可重复读幻读InnoDB 实现
读未提交 Read Uncommitted可能可能可能直接读最新版本
读已提交 Read Committed不会可能可能每条语句新建 ReadView
可重复读 Repeatable Read不会不会快照读不会,当前读可能事务首次读建 ReadView + 间隙锁
串行化 Serializable不会不会不会读也加共享 next-key lock
-- 查看与设置隔离级别(8.0 用 transaction_isolation)
SELECT @@transaction_isolation;
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
SET SESSION transaction_isolation = 'REPEATABLE-READ';

读已提交与可重复读的差别只有一个:ReadView 是每条语句建还是整个事务建。前者更新鲜,后者更稳定。

五、业务上怎么选

  • 默认用 RR:MVCC 让普通查询无锁,间隙锁能防住大部分写入型幻读。
  • 高并发写入、死锁频发时降到 RC:RC 不加间隙锁,锁范围小、死锁少,代价是同一事务内可能出现不可重复读,需要业务自己接受。
  • 报表、对账类必须一致:同一条只读事务里完成,或显式 START TRANSACTION WITH CONSISTENT SNAPSHOT。
  • 秒杀扣库存:不要依赖隔离级别,直接用 UPDATE ... WHERE stock > 0 的原子更新或版本号。

常见追问

  • RR 到底能不能防幻读? 快照读能(MVCC 快照不变),当前读靠间隙锁在多数场景能,但「先快照读、再当前读、再快照读」这类混合操作仍可能出现。
  • 为什么大厂常把默认级别改成 RC? 少间隙锁、少死锁、主从复制在 row 格式下更一致;代价是业务要接受不可重复读。
  • MVCC 能解决脏读吗? 能,未提交版本对 ReadView 不可见;但读未提交级别会绕过 MVCC。
  • 间隙锁一定存在吗? 只在 RR 及以上、且走非唯一索引或范围条件时出现;唯一索引等值命中时退化为记录锁。
  • 不可重复读和幻读的本质区别? 前者针对「同一行被修改」,后者针对「结果集行数变化」,对应的锁手段也不同。

小结

记忆锚点:脏读是「读到还没定的东西」,不可重复读是「同一行前后不一样」,幻读是「同一条件前后行数不一样」。回答时把级别、MVCC 快照、next-key lock 三者串起来,再补一句 RC 与 RR 的差别在 ReadView 的创建时机,基本就完整了。

笔记加载中…