1. MVCC 核心思想
MVCC(Multi-Version Concurrency Control) 通过保存数据的多个历史版本,实现 读不阻塞写、写不阻塞读,避免加锁带来的并发性能损失。
无 MVCC(锁模型):
事务 A 读 Row X ──→ 加读锁
│
事务 B 写 Row X ──→ 阻塞等待读锁释放
有 MVCC(版本链):
事务 A 读 Row X ──→ 获取历史版本 V1(无需加锁)
│
事务 B 写 Row X ──→ 创建新版本 V2(事务 B 的 trx_id)
↓
V1 保留在 Undo Log 中,供 A 继续读取
2. 版本链与 Undo Log
2.1 行结构
InnoDB 每行存储的隐藏列:
┌───────────┬───────────┬───────────┬────────┐
│ DB_TRX_ID │ DB_ROLL_PTR│ ...数据列... │ 主键 │
│ (6B) │ (7B) │ │ │
└───────────┴───────────┴───────────┴────────┘
DB_TRX_ID:最后修改该行的事务 ID
DB_ROLL_PTR:指向 Undo Log 的指针
2.2 版本链形成
初始状态:
Row: [trx_id=50, roll_ptr=NULL, value='Alice']
事务 100 更新:
UPDATE users SET name = 'Bob' WHERE id = 1;
→ 创建 Undo Log 记录旧值:
Undo Log: [trx_id=50, value='Alice', prev_ptr=NULL]
→ 更新行:
Row: [trx_id=100, roll_ptr→Undo Log, value='Bob']
事务 101 再更新:
UPDATE users SET name = 'Cathy' WHERE id = 1;
→ 创建新 Undo Log 记录旧值:
Undo Log 2: [trx_id=100, value='Bob', prev_ptr→Undo Log 1]
→ 更新行:
Row: [trx_id=101, roll_ptr→Undo Log 2, value='Cathy']
版本链:
Row → Undo Log 2 (trx=100, val='Bob') → Undo Log 1 (trx=50, val='Alice') → NULL
3. ReadView 与可见性判断
3.1 ReadView 结构
ReadView(快照):
┌──────────────┬──────────────┬──────────────┐
│ creator_trx_id │ min_trx_id │ max_trx_id │ m_ids │
│ 创建者事务ID │ 最小活跃事务 │ 最大待分配ID │ 活跃列表 │
└──────────────┴──────────────┴──────────────┘
m_ids:创建 ReadView 时所有未提交的事务 ID 列表
3.2 可见性判断规则
对于某行的 trx_id(最后修改它的事务 ID):
1. trx_id == creator_trx_id → 可见(自己修改的)
2. trx_id < min_trx_id → 可见(事务已提交)
3. trx_id >= max_trx_id → 不可见(事务在 ReadView 创建后启动)
4. min_trx_id <= trx_id < max_trx_id:
- trx_id 在 m_ids 中 → 不可见(事务活跃,未提交)
- trx_id 不在 m_ids 中 → 可见(事务已提交)
5. 如果不可见 → 沿 roll_ptr 回溯 Undo Log,找上一个版本
→ 重复判断直到找到可见版本或链结束
3.3 RC vs RR 隔离级别
| 隔离级别 | ReadView 生成时机 | 效果 |
|---|---|---|
| READ COMMITTED | 每条 SELECT 语句 | 每次读都看到最新已提交数据(不可重复读) |
| REPEATABLE READ | 事务首次 SELECT | 整个事务看到同一快照(可重复读) |
RC 场景(ReadView 每次重新生成):
T1 时刻:ReadView 看到 value='Alice'
T2 时刻:另一事务提交 value='Bob'
T3 时刻:新 ReadView 看到 value='Bob' ← 不可重复读
RR 场景(ReadView 只生成一次):
T1 时刻:ReadView 看到 value='Alice'
T2 时刻:另一事务提交 value='Bob'
T3 时刻:同一 ReadView 仍看到 value='Alice' ← 可重复读
4. MVCC 与幻读
MVCC 解决的是快照读(Snapshot Read / Consistent Read)的幻读问题。对于当前读(Current Read),InnoDB 使用**间隙锁(Gap Lock)**解决幻读。
-- 快照读(SELECT):通过 MVCC + ReadView,RR 下无幻读
SELECT * FROM orders WHERE user_id = 100;
-- 当前读(FOR UPDATE / LOCK IN SHARE MODE):通过 Next-Key Lock
SELECT * FROM orders WHERE user_id = 100 FOR UPDATE;
→ 在 user_id=100 的记录上加 Record Lock
→ 在相邻的空隙上加 Gap Lock(阻止新记录插入 user_id=100 的位置)
5. 总结
MVCC 核心机制:
写 → 创建新版本 + Undo Log(旧版本保留)
读 → ReadView 判断可见版本(可能回溯 Undo Log)
带来的好处:
✅ 读写不互斥(读不阻塞写,写不阻塞读)
✅ 无需读锁,并发性能高
✅ RR 级别天然防快照读幻读
代价:
❌ Undo Log 膨胀(长事务导致清理不了旧版本)
❌ 查询需回溯版本链(极端情况下性能下降)
❌ 不能替代写锁(当前读仍需加锁)
生产注意:
避免长事务!长事务持有的 ReadView 阻止 Undo Log 清理,
导致 Undo 表空间膨胀,影响性能和空间。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。