MySQL 事务隔离级别有哪些?分别解决什么问题?
简化版
四个隔离级别从低到高:读未提交(Read Uncommitted)→ 读已提交(Read Committed)→ 可重复读(Repeatable Read)→ 串行化(Serializable),分别对应解决脏读、不可重复读、幻读这三类问题。级别越高越一致,但并发性能越差。InnoDB 默认是可重复读(RR),且靠 MVCC + next-key lock 在 RR 下就基本解决了幻读。
详细版
先搞清三个「读的问题」:
- 脏读:读到了别的事务还没提交的数据,对方一旦回滚,你读到的就是从未存在过的「幽灵数据」。
- 不可重复读:同一事务内两次读同一行,中间别的事务改了并提交,导致两次结果不同(针对 UPDATE)。
- 幻读:同一事务内两次按同一条件查询,中间别的事务插入/删除了符合条件的行,导致两次的行数不同(针对 INSERT/DELETE)。
每个级别解决什么:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | ❌ 会 | ❌ 会 | ❌ 会 |
| 读已提交(RC) | ✅ 防住 | ❌ 会 | ❌ 会 |
| 可重复读(RR) | ✅ | ✅ 防住 | ⚠️ 标准会,InnoDB 基本防住 |
| 串行化 | ✅ | ✅ | ✅ 全防住 |
InnoDB 的一致性读靠 MVCC:每行存版本信息,普通的快照读按 Read View 找「对当前事务可见的版本」,不加锁、不阻塞写。而 select ... for update、update 这类当前读读的是最新版本并加锁。
完整版教学
一、MVCC:为什么读不加锁也能一致
MVCC(多版本并发控制)是理解 InnoDB 隔离的钥匙。核心思想:写不阻塞读,读不阻塞写。
每行数据除了本身,还隐藏着两个字段:trx_id(哪个事务改的)和 roll_pointer(指向 undo log 里的旧版本,串成一条版本链)。当你做一次快照读,InnoDB 生成一个 Read View,记录「此刻哪些事务还没提交」。读某行时沿版本链找到「对我可见的那个版本」——从而读到一个一致的快照,完全不用等别人的锁。
- RC 级别:每次快照读都生成新的 Read View → 所以能读到别人最新提交的 → 会「不可重复读」。
- RR 级别:只在事务第一次快照读时生成一次 Read View,之后一直用它 → 整个事务看到的是同一个快照 → 保证「可重复读」。
这就是 RC 和 RR 差异的本质:Read View 的生成时机不同,一个每次都建、一个只建一次。
用具体数字看:账户余额初始为 100。事务 T1 第一次普通查询读到 100;事务 T2 把余额改成 80 并提交。若 T1 是 RC,第二次普通查询会创建新的 Read View,可能读到 80;若 T1 是 RR,第二次仍沿用第一次的 Read View,继续读到 100。这里没有谁“更正确”,只是隔离级别承诺不同。
RC:
T1 select -> 100
T2 update 80 commit
T1 select -> 80
RR:
T1 select -> 100
T2 update 80 commit
T1 select -> 100
二、InnoDB 在 RR 下怎么解决幻读
按 SQL 标准,幻读要到「串行化」才能根除,但 InnoDB 在 RR 就基本搞定了,靠两条:
- 快照读(普通 select):靠 MVCC 读固定快照,别人插入的新行不在我的 Read View 里,天然看不见,没有幻读。
- 当前读(
for update、update):靠 next-key lock(间隙锁 + 行锁)锁住一个范围,别的事务无法在这个区间里插入新行,从而防住幻读。
举例:select * from t where id > 10 for update,InnoDB 会锁住 (10, +∞) 这段间隙,别人想插 id=15 会被阻塞。
这里要分清“快照读”和“当前读”。普通 select 看到的是同一个快照,所以别的事务插入的新行不在快照里;for update、update、delete 要处理最新数据,不能只读旧版本,所以需要记录锁、间隙锁、next-key lock 来保护范围。很多争议来自把这两类读混在一个例子里。
| 读类型 | 典型语句 | RR 下防幻读方式 |
|---|---|---|
| 快照读 | select * from t where age > 18 | MVCC 固定 Read View |
| 当前读 | select * from t where age > 18 for update | next-key lock 锁范围 |
| 修改读 | update t set ... where age > 18 | 当前读并加锁 |
三、脏读为什么绝对不能忍
读未提交允许读到未提交数据,问题在于:那个事务可能回滚。比如你读到「账户余额被加了 1000」并据此放货,结果对方事务回滚、那 1000 根本没到账——你基于一个从未真实存在的数据做了决策。所以生产环境几乎没人用读未提交。
脏读的危险在于它破坏了“提交才生效”的基本直觉。假设库存事务把库存从 0 临时改到 10,业务查询读到了 10 并允许下单,但随后库存事务回滚,真实库存仍是 0。这类错误不是展示层小问题,而是会直接造成资损或业务状态错乱。因此隔离级别再低,生产也通常不会选择读未提交。
不可重复读和幻读虽然也会影响业务判断,但它们读到的是已经提交的数据;脏读读到的是根本可能不存在的数据,所以风险等级更高。
四、隔离级别不是越高越好
串行化能防住一切并发问题,但它对读也加锁、几乎把事务串行执行,并发性能极差。别用「提升隔离级别」来解决业务竞态——那是杀鸡用牛刀且拖垮性能。
更好的做法是针对性地上手段:
- 防重复下单 → 唯一索引;
- 防超卖 → 条件更新
update stock set n=n-1 where id=? and n>0(靠行锁 + 原子判断); - 特定热点 → 悲观锁
for update或乐观锁(版本号)。
记忆点:隔离级别管「通用的读一致性」,具体业务竞态靠唯一索引、条件更新、乐观/悲观锁来兜。 两者分工,别混。
比如防超卖时,把隔离级别从 RR 提到 Serializable 不是首选。更稳的 SQL 是:
update sku_stock
set stock = stock - 1
where sku_id = 1001
and stock > 0;
这条语句把“检查库存”和“扣减库存”合成一次原子更新,受影响行数为 1 表示扣减成功,为 0 表示库存不足。它解决的是业务并发竞争,不是单靠隔离级别兜底。
五、四个隔离级别逐级看
隔离级别可以按“允许看到多新的并发修改”来理解。读未提交几乎不隔离,别人没提交也能看;读已提交只看已提交,所以防住脏读;可重复读在事务内保持同一快照,所以防住不可重复读;串行化最严格,通过更强锁行为让并发事务像排队执行。
| 隔离级别 | 普通读视角 | 典型问题 | InnoDB 常见使用感受 |
|---|---|---|---|
| RU | 可读未提交 | 脏读、不可重复读、幻读 | 很少生产使用 |
| RC | 每次读看最新已提交 | 不可重复读 | Oracle/PG 常见默认级别 |
| RR | 事务内快照稳定 | 标准意义可能幻读 | MySQL InnoDB 默认 |
| Serializable | 强制串行化效果 | 并发性能低 | 特殊强一致场景才考虑 |
这张表不是为了让你死背,而是帮助你把问题按层次归因。隔离级别越高,并发事务之间互相影响越少,但锁等待、阻塞和吞吐损耗也更明显。
六、快照读、当前读和业务一致性边界
InnoDB 默认 RR 能让普通 select 在同一事务内读到稳定快照,但当前读仍要读最新版本并加锁。比如先普通查询库存为 10,另一个事务扣成 9 并提交,再执行 update stock set stock=stock-1 where id=1,这条 update 是当前读,会基于最新记录进行加锁和修改,而不是执着于第一次快照里的 10。
这也说明事务隔离不是业务一致性的全部。订单不能重复创建,要靠唯一索引;库存不能扣成负数,要靠条件更新;账户余额不能并发覆盖,要靠行锁、乐观锁或流水模型。隔离级别提供数据库通用承诺,业务规则还需要明确约束。
七、常见误区与追问
- 误区:MySQL InnoDB 默认隔离级别是 RC。 InnoDB 默认是 RR,Oracle 和 PostgreSQL 常见默认是 RC,不同数据库不要混记。
- 误区:幻读必须靠 Serializable 才能解决。 SQL 标准层面 Serializable 最严格,但 InnoDB 在 RR 下通过 MVCC 和 next-key lock 基本解决常见幻读场景。
- 误区:不可重复读和幻读是一回事。 不可重复读关注同一行值被 update 后两次读取不同,幻读关注同一条件范围内 insert/delete 导致行集合变化。
- 误区:提升隔离级别能替代业务约束。 防重复、防超卖、防并发覆盖需要唯一索引、条件更新、版本号或锁策略,隔离级别不能表达所有业务规则。
- 追问:RC 和 RR 的核心差异是什么? 核心是 Read View 生成时机:RC 每次快照读生成,RR 通常事务第一次快照读生成并复用。
- 追问:RR 下普通 select 和
for update有什么不同? 普通 select 是快照读,按 Read View 找可见版本;for update是当前读,读最新已提交版本并加锁。
八、加强记忆
事务隔离级别按 RU、RC、RR、Serializable 逐级增强,分别围绕脏读、不可重复读、幻读这些并发读问题展开。InnoDB 默认 RR,普通快照读靠 MVCC 和 Read View 保持事务内视角稳定,当前读靠锁尤其是 next-key lock 保护范围。记忆时抓住三组边界:RC 与 RR 的 Read View 时机不同,快照读与当前读机制不同,数据库隔离与业务约束职责不同。