03.2 - 事务隔离级别
定位:隔离级别是事务并发控制的策略选择——不同级别在"一致性"和"性能"之间做出不同取舍 面试高频度:⭐⭐⭐⭐⭐ 考查方式:四种隔离级别能解决什么问题、RC 和 RR 的区别、MySQL 默认 RR 的原因、间隙锁与幻读
一、这是什么?为什么需要它?
是什么
事务隔离级别定义了一个事务的修改在什么时候对其他事务可见。SQL 标准定义了四种级别,从低到高:
| 级别 | 隔离程度 | 并发性能 |
|---|---|---|
| READ UNCOMMITTED | 最低 | 最高 |
| READ COMMITTED | 低 | 高 |
| REPEATABLE READ | 高 | 中 |
| SERIALIZABLE | 最高 | 最低 |
为什么需要不同隔离级别?
"强一致性"和"高性能"天生矛盾:
- 最强隔离(SERIALIZABLE):所有事务串行执行 → 不会出错但极慢
- 最弱隔离(READ UNCOMMITTED):所有事务并行执行 → 最快但数据可能混乱
不同业务场景需要不同权衡:
- 银行转账:宁愿慢也要 100% 正确(SERIALIZABLE 或 RR)
- 日志系统:丢了不太重要、写入性能更重要(RC 甚至 RU)
- 大多数 Web 应用:RC(大多数数据库默认)或 RR(MySQL 默认)
核心 insight:隔离级别的选择本质是"在可能的并发异常和性能之间做 trade-off"。没有"最好"的隔离级别,只有"最适合业务场景"的隔离级别。
二、原理拆解
2.1 三种并发异常
脏读(Dirty Read)
事务 A: 事务 B:
BEGIN; BEGIN;
UPDATE users SET age=30 WHERE id=1;
(未提交)
SELECT age=30 → 读到未提交的数据!
ROLLBACK; (回滚)
age=20(恢复)
脏读:读到了另一个事务未提交的数据——这个数据可能被回滚,是无效的为什么危险:如果系统根据脏数据做决策(如转账金额),而那个事务回滚了——决策就是错的。
不可重复读(Non-Repeatable Read)
事务 A: 事务 B:
BEGIN; BEGIN;
SELECT age=20;
UPDATE users SET age=30 WHERE id=1;
COMMIT;
SELECT age=30 → 同一事务内两次读不一样!
不可重复读:同一事务内两次读同一行,结果不一样(行被其他事务修改并提交)幻读(Phantom Read)
事务 A: 事务 B:
BEGIN; BEGIN;
SELECT * FROM users WHERE age > 18;
→ 返回 5 行
INSERT INTO users (id, age) VALUES (100, 20);
COMMIT;
SELECT * FROM users WHERE age > 18;
→ 返回 6 行 → 多了一行"幻影"行!
幻读:同一事务内两次查询,结果集的行数不一样(其他事务插入了新行)不可重复读 vs 幻读的区别:
| 对比维度 | 不可重复读 | 幻读 |
|---|---|---|
| 影响范围 | 一行 | 多行(结果集) |
| 操作类型 | UPDATE(修改已有行) | INSERT/DELETE(增加/删除行) |
| 如何解决 | MVCC 快照读 | MVCC + 间隙锁 |
2.2 四种隔离级别的细节
READ UNCOMMITTED(读未提交)
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;| 表现 | 后果 |
|---|---|
| 能读到其他事务未提交的数据 | 脏读、不可重复读、幻读全部可能发生 |
| 不加锁,性能最高 | 几乎不用,除非对数据一致性完全没要求 |
READ COMMITTED(读已提交)
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;| 表现 | 后果 |
|---|---|
| 只能读到其他事务已提交的数据 | ✅ 避免脏读 |
| 每次 SELECT 重新创建 Read View | ❌ 不可重复读、幻读仍可能 |
| 大多数数据库默认级别(Oracle, PostgreSQL, SQL Server) | MySQL 默认是 RR,RC 也很常用 |
RC 下的 MVCC 行为:
T1: BEGIN;(事务 200)
T2: 事务 201 UPDATE age=25, COMMIT
T3: SELECT → 创建 Read View v1 → age=25(可见)
T4: 事务 202 UPDATE age=30, COMMIT
T5: SELECT → 创建 Read View v2 → age=30(可见)
RC 下每条语句都创建新 Read View → 总能看到最新已提交的数据REPEATABLE READ(可重复读)
sql
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;MySQL InnoDB 默认隔离级别。
| 表现 | 后果 |
|---|---|
| 同一事务内多次读同一行结果一致 | ✅ 避免脏读 + 不可重复读 |
| 整个事务复用同一个 Read View | ✅ MVCC 快照读的一致性保证 |
| 通过间隙锁解决幻读 | ⚠️ 仅在当前读时保证 |
RR 下的 MVCC 行为:
T1: BEGIN;(事务 200)
T2: 事务 201 UPDATE age=25, COMMIT
T3: SELECT → 创建 Read View v1 → age=20(RR 复用快照)
T4: 事务 202 UPDATE age=30, COMMIT
T5: SELECT → 复用 Read View v1 → age=20(还是事务开始时的值)
RR 下复用第一个 Read View → 即使其他事务提交,也不影响当前事务的快照RR 如何解决幻读?
sql
-- 事务 A(RR 下)
BEGIN;
SELECT * FROM users WHERE age > 18 FOR UPDATE;
-- 这行 "当前读" 会对 age > 18 的行加锁,且对范围加间隙锁
-- 事务 B
INSERT INTO users (id, age) VALUES (100, 20);
-- ❌ 阻塞等待!间隙锁阻止了插入
-- 但普通 SELECT(快照读)不会加间隙锁:
-- 如果只用 SELECT,其他事务可以插入新行,但在本事务的 MVCC 快照中不可见SERIALIZABLE(可串行化)
sql
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;| 表现 | 后果 |
|---|---|
所有普通 SELECT 自动转为 SELECT ... LOCK IN SHARE MODE | 读写全部加锁,并发度极低 |
| 没有并发问题 | 性能最差,几乎不用 |
实际使用建议:95% 的场景不需要 SERIALIZABLE。如果业务真的需要串行化,考虑用"乐观锁 + 重试"代替,性能好得多。
2.3 隔离级别速查表
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
|---|---|---|---|---|
| READ UNCOMMITTED | ❌ | ❌ | ❌ | 读不加锁 |
| READ COMMITTED | ✅ | ❌ | ❌ | 每条语句 MVCC (RC) |
| REPEATABLE READ | ✅ | ✅ | ✅(InnoDB) | 事务级 MVCC + 间隙锁 (RR) |
| SERIALIZABLE | ✅ | ✅ | ✅ | 全部加锁 |
✅ = 不会发生,❌ = 可能发生
2.4 MySQL 默认 RR 的原因
| 为什么 MySQL 选 RR | 为什么 Oracle/PostgreSQL 选 RC |
|---|---|
| 历史原因:Binlog 复制在 RC 下可能出现主从不一致(statement 格式) | 对大多数应用足够且性能更优 |
| 8.0 之前 statement 格式 binlog 在 RC 下不安全 | 没有这个历史包袱 |
| 现在 row 格式 binlog 下,RC 也安全了 | — |
| 但仍保留 RR 为默认(兼容性) | — |
8.0 的推荐:如果不需要 RR,建议用 RC + row 格式 binlog,性能更好。
三、图解全景
事务隔离级别全景图
┌──────────────────────────────────────────────────────────┐
│ │
│ 并发问题 ↑ SERIALIZABLE │
│ │ │ │
│ 幻读 │ ┌───────┴───────┐ │
│ │ │ REPEATABLE │ │
│ 不可重复读│ ┌────┴────┐ READ │ │
│ │ │ READ │ (RR) │ │
│ 脏读 │ ┌────┴────┐ COMMITTED │ │
│ │ │ READ │ (RC) │ │
│ │ ┌────┴────┐ UNCOMMITTED │ │
│ │ │ (RU) │ (最弱) │ │
│ ▼ └─────────┴──────────────────┘ │
│ ↓性能下降 ↓ │
│ ↓隔离性上升 ↓ │
└──────────────────────────────────────────────────────────┘ MySQL 隔离级别决策树
业务是否需要?
│
├── 需要强一致性读(多次读必须一样)→ RR(Repeatable Read)
│ ↓
│ 注意:RR 在快照读下由 MVCC 保证,当前读由间隙锁保证
│
├── 读最新已提交数据即可 → RC(Read Committed)
│ ↓
│ ✅ 大多数 Web 应用选 RC 就够
│ ✅ 性能比 RR 好(不需要间隙锁)
│ ⚠️ 不可重复读是否可接受?
│
├── 对一致性毫无要求 → RU(Read Uncommitted)
│ ↓
│ ⚠️ 几乎不用,除非你知道自己在做什么
│
└── 坚决不允许任何并发问题 → Serializable
↓
⚠️ 性能极差,考虑用乐观锁替代四、实战验证
测试脏读
sql
-- 会话 A
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT * FROM users WHERE id = 1;
-- 此时 age=20
-- 会话 B(另一个连接)
BEGIN;
UPDATE users SET age = 999 WHERE id = 1;
-- 未提交
-- 会话 A 再次查询
SELECT * FROM users WHERE id = 1;
-- 如果读到 age=999 → 脏读!
-- 这是 RU 级别,不推荐
-- 会话 B
ROLLBACK;验证 RC vs RR 的读差异
sql
-- 准备数据
CREATE TABLE test_isolation (
id INT PRIMARY KEY,
val INT
);
INSERT INTO test_isolation VALUES (1, 100);
-- ===== 测试 RR =====
-- 会话 A
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT val FROM test_isolation WHERE id = 1; -- 100
-- 会话 B
UPDATE test_isolation SET val = 200 WHERE id = 1;
COMMIT;
-- 会话 A
SELECT val FROM test_isolation WHERE id = 1;
-- RR: 100(事务内快照不变)
COMMIT;
-- ===== 测试 RC =====
-- 会话 A
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT val FROM test_isolation WHERE id = 1; -- 100
-- 会话 B
UPDATE test_isolation SET val = 200 WHERE id = 1;
COMMIT;
-- 会话 A
SELECT val FROM test_isolation WHERE id = 1;
-- RC: 200(每次读最新已提交数据)
COMMIT;测试幻读(RR vs RC)
sql
-- 准备
CREATE TABLE test_phantom (
id INT PRIMARY KEY,
name VARCHAR(10)
);
INSERT INTO test_phantom VALUES (1, 'a'), (2, 'b'), (3, 'c');
-- 事务 A (RR)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM test_phantom; -- 3
-- 事务 B
INSERT INTO test_phantom VALUES (4, 'd');
COMMIT;
-- 事务 A 再次查询
SELECT COUNT(*) FROM test_phantom;
-- RR 下: 3(MVCC 快照,看不到新行)
UPDATE test_phantom SET name = 'x'; -- 当前读!
SELECT COUNT(*) FROM test_phantom;
-- RR 下: 4(当前读看到最新数据,且间隙锁阻止进一步插入)
COMMIT;五、面试视角
| 追问 | 答案要点 |
|---|---|
| 四种隔离级别分别解决什么问题? | RU: 不防;RC: 防脏读;RR: 防脏读+不可重复读;SERIALIZABLE: 全防 |
| 不可重复读和幻读的区别? | 前者是同一行数据变化(UPDATE),后者是结果集行数变化(INSERT/DELETE) |
| MySQL 为什么默认 RR? | 历史原因——statement 格式 binlog 下 RC 不安全。现在 row 格式下 RC 也可用 |
| RR 怎么解决幻读? | MVCC 解决快照读的幻读(看不到新行),间隙锁解决当前读的幻读(阻止插入) |
| 怎么选择 RC 和 RR? | 业务需要事务内一致性读存照→RR;只要最新数据→RC(性能更好) |
| 什么情况下选 SERIALIZABLE? | 几乎不用。宁可用"乐观锁+应用层重试"代替 |
| 间隙锁对性能的影响? | 增加锁冲突概率,降低并发度。RC 没有间隙锁,所以高并发场景 RC 通常更快 |
📚 相关链接
- MVCC 原理:**MVCC实现原理** — 隔离级别的实现基础
- ACID 实现:**事务ACID实现** — 完整的事务保证
- 锁机制:**行锁与间隙锁** — 间隙锁详解
- ← 返回 **SQL与事务索引**