一、为什么需要 MVCC?
在没有MVCC 的年代,数据库主要靠行锁/表锁来保证事务隔离性。例如:
-- 事务 A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id =1; -- 获取写锁
-- 事务B(被阻塞)
BEGIN;
SELECT balance FROM accounts WHERE user_id = 1; -- 等待读锁释放
这种模式存在明显问题:
- 读写互斥:写操作会阻塞所有读操作;
- 并发度低:大量 SELECT 请求排队等待;
- 死锁风险:多个事务交叉加锁容易形成环路。
而 MVCC 的核心思想是:不修改原数据,而是保留多个历史版本,让读操作看到“合适”的版本,从而避免加锁。
✅ MVCC 的最大优势:读不加锁,读写不冲突!
二、MVCC 只在哪些隔离级别下生效?
MySQL 支持四种事务隔离级别,但 MVCC 仅在以下两种级别中工作:
| 隔离级别 | 是否使用 MVCC | 说明 |
|---|---|---|
| READ UNCOMMITTED | ❌ | 直接读最新数据(可能未提交),无视版本 |
| READ COMMITTED (RC) | ✅ | 每次 SELECT 都生成新 ReadView |
| REPEATABLE READ(RR) | ✅ | 事务首次SELECT 时生成ReadView,后续复用 |
| SERIALIZABLE | ❌ | 所有读都加共享锁,退化为锁机制 |
📌 注意:InnoDB 默认隔离级别是 REPEATABLE READ,这也是 MVCC 发挥最大价值的场景。
三、MVCC 的三大核心组件
InnoDB 实现 MVCC依赖三个关键技术:
1.隐藏字段(每行数据自带)
InnoDB 为每一行记录自动添加以下隐藏列:
DB_TRX_ID(6 字节):最后一次修改该行的事务 IDDB_ROLL_PTR(7 字节):回滚指针,指向 undo log中的上一个版本DB_ROW_ID(7 字节):无主键时用于生成聚簇索引(非 MVCC 核心)
💡 事务ID 是全局递增的整数,由InnoDB 内部维护。
2. Undo Log +版本链(VersionChain)
当执行 UPDATE 或 DELETE 时:
- 原始数据不会被覆盖,而是拷贝到 undolog 中;
- 新版本写入数据页,并更新
DB_TRX_ID和DB_ROLL_PTR; DB_ROLL_PTR指向 undolog 中的旧版本,形成一条 版本链。
例如:
当前行 (trx_id=102) → undolog (trx_id=101) → undo log(trx_id=99) → ...
这样,通过链式结构可以回溯任意历史版本。
3. ReadView(读视图)
ReadView 是 MVCC 判断“哪个版本可见”的关键。它在事务执行第一个快照读(如普通 SELECT)时创建,包含以下信息:
m_ids:当前活跃(未提交)的事务 ID 列表min_trx_id:m_ids中最小的事务 IDmax_trx_id:下一个将分配的事务 ID(即当前最大 ID+ 1)creator_trx_id:当前事务自己的ID
可见性判断规则(对某行数据的 trx_id):
- 如果
trx_id == creator_trx_id→ 自己修改的,可见 - 如果
trx_id < min_trx_id→ 在 ReadView 创建前已提交,可见 - 如果
trx_id >= max_trx_id→ 未来事务创建的,不可见 - 如果
trx_id ∈ m_ids→ 其他活跃事务修改的,不可见 - 否则(
trx_id < max_trx_id且不在m_ids中)→ 已提交,可见
若当前版本不可见,则沿 DB_ROLL_PTR向上查找历史版本,直到找到可见版本或链结束。
四、RC与 RR 的 MVCC 行为差异
| 特性 | READ COMMITTED (RC) | REPEATABLE READ (RR) |
|---|---|---|
| ReadView 创建时机 | 每次 SELECT 都新建 | 事务首次SELECT 时创建,后续复用 |
| 是否可重复读 | ❌(可能看到其他事务新提交的数据) | ✅(始终看到同一快照) |
| 幻读问题 | 存在(范围查询可能新增行) | 快照读无幻读(但当前读仍可能有) |
🔍 举例:事务 A 在 RR下两次 SELECT同一行,即使事务B 中间 UPDATE并 COMMIT,A仍看到第一次的结果。
五、Purge 线程:清理无用版本
Undolog 不会无限增长。InnoDB 有后台 purge线程,定期清理:
- 已被标记删除(
deleted_bit=true) - 且对所有活跃事务都不可见的历史版本
这确保了存储空间的有效回收。
六、总结:MVCC 的本质
MVCC 本质上是一种 基于时间戳(事务ID)的乐观并发控制。它通过:
- 保留多版本数据(undo log +版本链)
- 动态判断可见性(ReadView)
- 避免读写加锁
从而在保证隔离性的同时,极大提升了并发性能,特别适合 读多写少 的 OLTP场景。
⚠️但注意:MVCC 只解决快照读(普通 SELECT)的并发问题。对于
SELECT ... FOR UPDATE、UPDATE、DELETE等 当前读(Current Read),仍需加锁!
延伸思考
- 为什么 SERIALIZABLE 不用MVCC?→ 因为它要求完全串行,必须加锁。
- Undo log 是逻辑日志还是物理日志?→ 逻辑日志(记录反向 SQL操作)。
- MVCC 能解决幻读吗?→ 在 RR 下,快照读可以;但当前读仍需间隙锁配合。