面试必备:MySQL 事务隔离级别详解
什么是事务?
事务是数据库操作的最小工作单元,要么全部成功,要么全部失败。
-- 转账场景
BEGIN; -- 开启事务
UPDATE account SET balance = balance - 1000 WHERE id = 1; -- 张三减1000
UPDATE account SET balance = balance + 1000 WHERE id = 2; -- 李四加1000
COMMIT; -- 提交
-- 如果中途失败,执行 ROLLBACK 回滚
事务四大特性(ACID)
| 特性 | 说明 | 保障机制 |
|---|---|---|
| Atomic(原子性) | 全部成功或全部失败 | Undo Log |
| Consistency(一致性) | 事务前后数据状态一致 | 其他特性保证 |
| Isolation(隔离性) | 并发事务相互隔离 | 锁 + MVCC |
| Durability(持久性) | 提交后数据持久保存 | Redo Log |
并发问题
多个事务同时操作数据时,会产生问题:
1. 脏读(Dirty Read)
读到其他事务未提交的数据。
时间线: 事务 A 事务 B
─────────────────────────────────────────────
T1 BEGIN;
T2 SELECT balance BEGIN;
T3 WHERE id = 1;
T4 balance = 900 UPDATE balance = 800
T5 ← 看到 B 修改的值!
T6 ROLLBACK; ← B 回滚了
T7 balance = 900 -- 但 A 已经读到脏数据
↑ 错误!数据根本不存在
2. 不可重复读(Non-repeatable Read)
同一事务中,两次读取同一行数据,结果不同(因为被其他事务修改了)。
时间线: 事务 A 事务 B
─────────────────────────────────────────────
T1 BEGIN;
T2 SELECT balance BEGIN;
T3 WHERE id = 1;
T4 balance = 1000 UPDATE balance = 900
T5 ← 1000 COMMIT;
T6 SELECT balance
T7 WHERE id = 1;
T8 ← 900 -- A 两次读到的值不一样!
3. 幻读(Phantom Read)
同一事务中,两次查询结果集不同(因为被其他事务插入了新行)。
时间线: 事务 A 事务 B
─────────────────────────────────────────────
T1 BEGIN;
T2 SELECT * BEGIN;
T3 FROM user
T4 WHERE age > 20; INSERT INTO user ...
T5 ← 返回 5 条 COMMIT;
T6 SELECT *
T7 FROM user
T8 WHERE age > 20;
T9 ← 返回 6 条 -- A 读到了"幻影"
脏读 vs 不可重复读 vs 幻读
| 问题 | 原因 | 读到的内容 |
|---|---|---|
| 脏读 | 读了其他事务未提交的数据 | 别人的修改 |
| 不可重复读 | 其他事务修改了数据 | 同一行数据变化 |
| 幻读 | 其他事务插入/删除了数据 | 结果集数量变化 |
四种隔离级别
MySQL 提供四种隔离级别,越严格性能越低:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 最低,性能最高 |
| READ COMMITTED | 不可能 | 可能 | 可能 | Oracle 默认 |
| REPEATABLE READ | 不可能 | 不可能 | 可能 | MySQL 默认 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 最高,性能最低 |
1. READ UNCOMMITTED(读未提交)
最低级别,允许脏读。
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT balance FROM account WHERE id = 1; -- 可能读到其他事务未提交的值
COMMIT;
2. READ COMMITTED(读已提交)
解决脏读,但可能有不可重复读。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
-- 只能读到已提交的数据
COMMIT;
3. REPEATABLE READ(可重复读)
MySQL 默认级别,解决脏读和不可重复读,但可能有幻读。
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- 事务中多次读取,结果一致
SELECT * FROM user;
SELECT * FROM user; -- 和第一次结果一样
COMMIT;
MySQL 特殊:在可重复读下,MySQL 通过 MVCC + Next-Key Lock 基本解决了幻读。
4. SERIALIZABLE(串行化)
最高级别,完全串行执行,彻底解决问题,但性能最差。
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- 所有读操作都会加锁
COMMIT;
MVCC(多版本并发控制)
核心思想
每个事务看到的是快照数据,而不是实时的最新数据。
同一时刻,两个事务可能看到不同的数据版本:
事务 A (id=1) 事务 B (id=2) 数据库
─────────────────────────────────────────
SELECT balance SELECT balance
← 1000 ← 1000
UPDATE balance = 900
COMMIT;
SELECT balance SELECT balance
← 1000 (快照) ← 900 (新版本)
InnoDB 的 MVCC 实现
隐藏列
InnoDB 给每行数据添加了两个隐藏列:
┌──────────────────────────────────────────────┐
│ │
│ db_trx_id │ db_roll_ptr │ ...其他字段 │
│ (事务ID) │ (回滚指针) │ │
│ │
└──────────────────────────────────────────────┘
| 列 | 说明 |
|---|---|
| db_trx_id | 最近修改这行的事务 ID |
| db_roll_ptr | 指向 undo log 的指针 |
Undo Log(版本链)
每行数据通过 roll_ptr 形成版本链:
事务 C 修改
↓
┌─────────────────────────────────────────┐
│ 事务 A 插入: balance=1000 │
│ trx_id=A, roll_ptr=null │
└──────────────┬──────────────────────────┘
│ roll_ptr
▼
┌─────────────────────────────────────────┐
│ 事务 B 修改: balance=900 │
│ trx_id=B, roll_ptr→A │
└──────────────┬──────────────────────────┘
│ roll_ptr
▼
┌─────────────────────────────────────────┐
│ 事务 C 修改: balance=800 │
│ trx_id=C, roll_ptr→B │
│ ← 最新版本 │
└─────────────────────────────────────────┘
ReadView(读视图)
事务开启时生成 ReadView,记录"当前有哪些事务还没提交"。
ReadView {
m_ids: [活跃事务ID列表],
min_trx_id: 最小活跃事务ID,
max_trx_id: 最大事务ID,
creator_trx_id: 当前事务ID
}
判断规则:
├─ trx_id < min_trx_id → 已提交,可以读
├─ trx_id >= max_trx_id → 未来事务,不可见
├─ trx_id in m_ids → 未提交事务,不可见
└─ trx_id == creator_trx_id → 自己,可以读
不同隔离级别的 MVCC
| 隔离级别 | 读取时机 |
|---|---|
| READ UNCOMMITTED | 最新版本(不加锁) |
| READ COMMITTED | 每次 SELECT 生成新 ReadView |
| REPEATABLE READ | 事务开始时生成 ReadView,一直用 |
当前读 vs 快照读
快照读(普通 SELECT)
SELECT * FROM user WHERE id = 1; -- 快照读,读历史版本
当前读(加锁读取)
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE; -- 共享锁
SELECT * FROM user WHERE id = 1 FOR UPDATE; -- 排他锁
INSERT / UPDATE / DELETE; -- 自动加排他锁
面试高频问题
Q1:MySQL 默认隔离级别是什么?
REPEATABLE READ。
Oracle 默认是 READ COMMITTED。
Q2:如何解决幻读?
方案 1:MVCC(解决快照读的幻读)
- 快照读不会看到其他事务插入的数据
方案 2:Next-Key Lock(解决当前读的幻读)
- 锁定索引范围 + 间隙,防止插入
SELECT * FROM user WHERE age > 20 FOR UPDATE;
-- 锁定 age > 20 的范围,包括间隙,阻止 INSERT age > 20
Q3:隔离级别设置在哪里?
-- 查看当前会话隔离级别
SELECT @@tx_isolation;
-- 查看全局隔离级别
SELECT @@global.tx_isolation;
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Q4:MVCC 能否完全避免幻读?
不能。MVCC 只解决快照读的幻读。当前读仍可能幻读,但 Next-Key Lock 可以防止。
总结
四大隔离级别:
├─ READ UNCOMMITTED:最低,可能脏读
├─ READ COMMITTED:解决脏读,可能不可重复读
├─ REPEATABLE READ(MySQL默认):解决脏读+不可重复读
└─ SERIALIZABLE:最高,完全串行
并发问题:
├─ 脏读:读了别人未提交的数据
├─ 不可重复读:同一行数据两次读不一样
└─ 幻读:结果集数量变了
MVCC:
├─ 隐藏列:trx_id + roll_ptr
├─ Undo Log:版本链
├─ ReadView:判断版本可见性
└─ REPEATABLE READ:事务开始生成 ReadView
解决幻读:
├─ 快照读 → MVCC
└─ 当前读 → Next-Key Lock