💻CS 数据库
事务与隔离级别
难度:⭐⭐ | 高频指数:🔥🔥🔥
面试回答
常见问法
- 事务的 ACID 特性是什么?
- 四种隔离级别分别是什么?
- 脏读、不可重复读、幻读有什么区别?
- MySQL 默认隔离级别是什么?怎么实现的?
- MVCC 是什么?怎么工作的?
回答
ACID 四大特性:
| 特性 | 含义 | 实现方式 |
|---|---|---|
| 原子性(Atomicity) | 事务要么全做要么全不做 | undo log(回滚日志) |
| 一致性(Consistency) | 事务前后数据满足约束 | 由其他三个特性保证 |
| 隔离性(Isolation) | 并发事务互不干扰 | 锁 + MVCC |
| 持久性(Durability) | 提交后数据永久保存 | redo log(重做日志) |
四种隔离级别(从低到高):
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(Read Uncommitted) | ✓ | ✓ | ✓ |
| 读已提交(Read Committed) | ✗ | ✓ | ✓ |
| 可重复读(Repeatable Read) | ✗ | ✗ | ✓(InnoDB 通过间隙锁解决) |
| 串行化(Serializable) | ✗ | ✗ | ✗ |
MySQL InnoDB 默认隔离级别是可重复读(RR),并且通过 MVCC + 间隙锁在很大程度上解决了幻读问题。
追问
1. 脏读、不可重复读、幻读的区别?
- 脏读:读到了其他事务未提交的数据。如果那个事务回滚了,你读到的就是无效数据。
- 不可重复读:同一事务内两次读同一行,结果不同(被其他事务修改了)。
- 幻读:同一事务内两次范围查询,结果集行数不同(被其他事务插入/删除了)。
2. MVCC 是什么?
MVCC(Multi-Version Concurrency Control,多版本并发控制)让读操作不加锁,通过保存数据的多个版本来实现。每行数据有隐藏的版本号(事务 ID),读操作根据自己的”快照”决定能看到哪个版本。
3. Read View 是什么?
Read View 是事务在某个时刻的”快照”,记录了当时活跃的事务 ID 列表。通过 Read View 判断某个版本的数据对当前事务是否可见。
原理展开
1. 三种并发问题详解
-- 脏读示例
-- 事务 A 事务 B
BEGIN; BEGIN;
UPDATE accounts SET balance=0
WHERE id=1;
SELECT balance FROM accounts
WHERE id=1; -- 读到 0(未提交)
ROLLBACK; -- 事务 A 回滚了,但 B 已经用了错误数据
-- 不可重复读示例
-- 事务 A 事务 B
BEGIN; BEGIN;
SELECT balance FROM accounts
WHERE id=1; -- 读到 100
UPDATE accounts SET balance=200
WHERE id=1;
COMMIT;
SELECT balance FROM accounts
WHERE id=1; -- 读到 200(变了!)
-- 幻读示例
-- 事务 A 事务 B
BEGIN; BEGIN;
SELECT * FROM users
WHERE age > 20; -- 3 行
INSERT INTO users(age) VALUES(25);
COMMIT;
SELECT * FROM users
WHERE age > 20; -- 4 行(多了一行!)
2. MVCC 实现原理
InnoDB 每行数据的隐藏列:
DB_TRX_ID:最后修改该行的事务 IDDB_ROLL_PTR:指向 undo log 中该行的上一个版本DB_ROW_ID:隐藏主键(没有主键时使用)
版本链:
当前版本 → undo log 版本1 → undo log 版本2 → ...
(trx_id=5) (trx_id=3) (trx_id=1)
3. Read View 的可见性判断
Read View 包含:
m_ids:创建 Read View 时活跃的事务 ID 列表min_trx_id:活跃事务中最小的 IDmax_trx_id:下一个将分配的事务 IDcreator_trx_id:创建该 Read View 的事务 ID
判断规则:
对于某行数据的 trx_id:
1. trx_id < min_trx_id → 可见(事务在 Read View 创建前已提交)
2. trx_id >= max_trx_id → 不可见(事务在 Read View 创建后才开始)
3. min_trx_id <= trx_id < max_trx_id:
- trx_id 在 m_ids 中 → 不可见(事务还未提交)
- trx_id 不在 m_ids 中 → 可见(事务已提交)
4. trx_id == creator_trx_id → 可见(自己的修改)
4. RC 和 RR 的 MVCC 区别
Read Committed(RC):
- 每次 SELECT 都创建新的 Read View
- 所以能看到其他事务已提交的最新数据
- 解决脏读,但有不可重复读
Repeatable Read(RR):
- 只在事务第一次 SELECT 时创建 Read View
- 后续 SELECT 复用同一个 Read View
- 所以同一事务内看到的数据一致
- 解决脏读和不可重复读
5. undo log 和 redo log
undo log(回滚日志):
- 记录数据修改前的值
- 用于事务回滚(保证原子性)
- 用于 MVCC(提供历史版本)
- INSERT 对应的 undo log 在事务提交后可以立即删除
- UPDATE/DELETE 的 undo log 需要等没有事务引用时才能删除
redo log(重做日志):
- 记录数据页的物理修改
- 用于崩溃恢复(保证持久性)
- WAL(Write-Ahead Logging):先写日志再写数据页
- 顺序写入,性能高
6. 事务的实现流程
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
内部流程:
1. 在 undo log 中记录旧值(balance = 原值)
2. 修改 Buffer Pool 中的数据页
3. 在 redo log buffer 中记录修改
4. 事务提交时:redo log 刷盘(fsync)
5. 数据页异步刷盘(checkpoint)
崩溃恢复:
- 如果 redo log 中有该事务的 commit 记录 → 重做
- 如果没有 commit 记录 → 用 undo log 回滚
7. 隔离级别的选择
| 场景 | 推荐隔离级别 | 原因 |
|---|---|---|
| 大部分业务 | RR(MySQL 默认) | 平衡一致性和性能 |
| 对一致性要求不高 | RC | 性能更好,锁冲突少 |
| 金融转账 | RR 或 Serializable | 必须保证一致性 |
| 报表统计 | RC | 不需要可重复读 |
互联网公司常用 RC 的原因:
- 锁粒度更小(没有间隙锁),并发性能更好
- 配合乐观锁/业务层校验保证一致性
- 阿里巴巴等公司的 MySQL 规范推荐 RC
易错点
- 说”ACID 中的 C 是由数据库保证的”——一致性更多是应用层的责任,数据库通过 AID 来支撑。
- 混淆不可重复读和幻读——不可重复读是同一行数据变了,幻读是结果集行数变了。
- 说”RR 完全解决了幻读”——MVCC 的快照读解决了,但当前读(SELECT … FOR UPDATE)仍可能幻读,需要间隙锁。
- 不知道 MVCC 的实现——只说”多版本”不够,要能说出 Read View 和版本链。
- 说”隔离级别越高越好”——越高并发性能越差,要根据业务选择。
- 混淆 undo log 和 redo log 的作用——undo 保证原子性(回滚),redo 保证持久性(恢复)。
记忆技巧
- ACID 口诀:原(原子)一(一致)隔(隔离)持(持久)
- 三种问题:脏(读未提交的)、不重复(同行变了)、幻(行数变了)
- 隔离级别从低到高:RU → RC → RR → S
- MVCC 核心:版本链 + Read View + 可见性判断
- 两个 log:undo 管回滚,redo 管恢复
- RC vs RR 的 MVCC 区别:RC 每次读新快照,RR 复用第一次的快照
面试速答版
事务的 ACID:原子性(undo log 回滚)、一致性(由 AID 保证)、隔离性(MVCC + 锁)、持久性(redo log)。四种隔离级别从低到高:读未提交、读已提交、可重复读、串行化。脏读是读到未提交数据,不可重复读是同行数据变了,幻读是结果集行数变了。MySQL 默认 RR,通过 MVCC 实现:每行有版本链(undo log),事务通过 Read View 判断哪个版本可见。RC 每次 SELECT 创建新 Read View,RR 复用第一次的。InnoDB 在 RR 下通过间隙锁进一步解决幻读。
Related · 数据库