脏读、幻读、不可重复读是什么?
结论:三者都是并发事务互相“看见”对方未完成或已提交的修改所导致的现象。脏读是读到了别人未提交的数据;不可重复读是同一行两次读到的值不同;幻读是同一条件两次读到的行数不同。隔离级别逐级加严,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 的创建时机,基本就完整了。