Skip to content

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与事务索引**

Knowledge4J — Java 知识库