事务隔离级别 MVCC 原理
事务隔离级别与 MVCC 原理
在数据库系统中,事务并发执行时可能会引发一系列数据不一致问题。事务隔离级别定义了事务之间相互隔离的程度,而多版本并发控制(MVCC)正是实现这些隔离级别的核心技术之一。本教程将从并发问题出发,系统讲解四种隔离级别,并深入解析 MVCC 如何通过版本链、快照和可见性规则来高效地实现不同级别的隔离。
1. 并发事务带来的常见问题
当多个事务同时读写相同数据时,如果不加控制,会出现以下三种典型异常:
-
脏读(Dirty Read)
事务 A 读取到了事务 B 尚未提交的修改数据。如果 B 最终回滚,A 读到的就是无效的“脏数据”。 -
不可重复读(Non-Repeatable Read)
事务 A 内两次读取同一行数据,在两次读取之间,事务 B 修改了该行并提交,导致 A 两次读到的值不一致。 -
幻读(Phantom Read)
事务 A 按相同条件两次执行范围查询,在两次查询之间,事务 B 插入了满足该条件的新行并提交,导致 A 第二次查询看到了多出的“幻影行”。
理解这三种问题,是学习隔离级别和 MVCC 解决思路的基础。
2. 四种事务隔离级别
SQL 标准定义了四种隔离级别,由低到高分别是:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | ✔ 可能 | ✔ 可能 | ✔ 可能 |
| READ COMMITTED | ✘ 禁止 | ✔ 可能 | ✔ 可能 |
| REPEATABLE READ | ✘ 禁止 | ✘ 禁止 | ✔ 可能(部分实现避免) |
| SERIALIZABLE | ✘ 禁止 | ✘ 禁止 | ✘ 禁止 |
注意:不同数据库对隔离级别的实现存在差异。例如 MySQL InnoDB 的 REPEATABLE READ 通过间隙锁(Gap Lock)在很大程度上避免了幻读,但语义上仍允许某些场景出现幻影行;而 PostgreSQL 的 REPEATABLE READ 则完全杜绝了幻读。
隔离级别越严格,数据一致性越强,但并发性能越低。在实际系统中,READ COMMITTED 和 REPEATABLE READ 最为常用。
3. MVCC 核心思想
MVCC(Multi-Version Concurrency Control) 是一种“以空间换时间”的并发控制机制。它的核心思想是为每行数据保存多个版本,使得读操作无需加锁即可看到一个一致的数据快照,从而在不阻塞写操作的前提下实现事务隔离。
MVCC 主要依赖以下三个组件:
- 隐藏列:数据表中每一行都包含额外的系统列,例如
DB_TRX_ID(最后修改该行的事务 ID)、DB_ROLL_PTR(回滚指针,指向 undo 日志中的旧版本)等。 - Undo Log(版本链):记录行的历史版本。通过回滚指针将所有版本串联成一条链表,供读视图回溯。
- Read View(快照):事务启动时生成的一个可见性判断依据,定义了哪些事务修改的数据对本事务可见,哪些不可见。
4. MVCC 如何实现不同隔离级别
不同隔离级别的核心差异在于生成 Read View 的时机:
4.1 READ COMMITTED —— 每次查询生成新的 Read View
在 READ COMMITTED 级别下,一个事务内的每次普通 SELECT 语句都会重新生成一个 Read View。这样每次查询都能看到其他事务已经提交的最新数据,从而避免脏读,但无法避免不可重复读和幻读。
可见性判断规则(简化版):
- 数据行的最后修改事务 ID(
trx_id)若在 Read View 的活跃事务列表中,说明此修尚未提交,该版本不可见。 - 若
trx_id小于 Read View 的最小活跃事务 ID,且非活跃,说明已提交,可见。 - 若版本不可见,则顺着回滚指针找到前一个版本继续判断,直到找到可见版本。
4.2 REPEATABLE READ —— 事务内固定 Read View
在 REPEATABLE READ 级别下,一个事务只在第一次快照读时生成一个 Read View,后续所有快照读都复用该视图。这意味着在整个事务期间,看到的数据版本始终一致,有效避免了不可重复读。
对于幻读,部分数据库(如 PostgreSQL)通过完全依靠快照实现隔离,自动避免了幻读;而 MySQL InnoDB 在 REPEATABLE READ 下,快照读无法避免更新幻读场景,因此引入了间隙锁(Gap Lock)与临键锁(Next-Key Lock)来物理锁定范围,阻塞新记录的插入。
4.3 SERIALIZABLE —— 强制串行化
SERIALIZABLE 是最严格的级别。它通常通过严格两阶段锁(2PL)实现,所有读取都加共享锁,范围读取加范围锁,彻底杜绝并发问题。此时 MVCC 的作用退居次要,更多依赖锁机制。
4.4 READ UNCOMMITTED —— 不基于快照
READ UNCOMMITTED 基本不使用 MVCC 的快照机制,事务直接读取数据的最新版本,即使该版本未被提交。在 MySQL InnoDB 中,该级别下 SELECT 语句不加锁,且完全忽略 undo log,因此会出现脏读。
5. MVCC 的读写协同机制
MVCC 实现了写不阻塞读、读不阻塞写的高效并发:
- 更新操作:不会直接覆盖原始行,而是插入新版本,并修改回滚指针指向旧版本,同时通过 undo log 维护版本链。旧版本不会被立即删除,直到无任何事务需要访问它时由 purge 线程回收。
- 删除操作:通常标记为删除(写入
delete mark),并插入一个新版本,commit 后该行对新的 Read View 不可见。 - 快照读:基于 Read View 遍历版本链寻找可见版本,全程不加锁。
- 当前读(如
SELECT ... FOR UPDATE、UPDATE、DELETE):仍然读取最新版本,并加相应锁,遵循锁机制以保证最新状态。
6. 不同数据库的 MVCC 实现差异
MySQL InnoDB
- 通过
trx_id和回滚指针维护版本链,undo log 存储在共享表空间或独立 undo 表空间。 - REPEATABLE READ 是默认隔离级别。
- 结合间隙锁和临键锁来辅助防止幻读。
PostgreSQL
- 使用元组版本(tuple version)存储于数据页中,旧版本始终保留在原表,通过 VACUUM 清理。
- 并无实际的 undo log,版本信息直接存储,通过事务快照和可见性规则判断。
- READ COMMITTED 是默认级别,支持真正的 REPEATABLE READ 和可串行化。
Oracle
- 采用 Undo Segment 管理旧版本,读操作几乎从不被写操作阻塞。
- 默认使用 READ COMMITTED(无法脏读),也支持 SERIALIZABLE。
了解具体数据库的实现,有助于在实战中选择合适的隔离级别和优化策略。
7. 总结与最佳实践
- 理解问题本质:脏读、不可重复读、幻读是设计隔离策略的直接目标。
- 隔离级别是权衡:越高的一致性意味着越低的并发能力;绝大多数应用在 READ COMMITTED 下即可保证业务正确,同时获得良好性能。
- MVCC 使读写分离:通过多版本,让快照读免于加锁,极大提升了读密集场景的吞吐。
- 实战建议:
- 认真分析业务对一致性的真实要求,避免盲目使用 SERIALIZABLE。
- 在使用 REPEATABLE READ 时,注意其幻读陷阱,必要时通过显式锁定补充。
- 关注长事务对版本链堆积和系统回收的影响,及时提交或回滚。
掌握 MVCC 原理,你不仅能更透彻地理解事务行为,还能针对性地进行性能优化与故障排查。