05. MVCC 多版本并发控制

深入理解 InnoDB MVCC 机制:Undo Log 版本链、ReadView 可见性判断算法、不同隔离级别下的行为差异。

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 表空间膨胀,影响性能和空间。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. 缓存架构演进之路:从单机 Redis 到亿级分布式多级缓存体系
  2. Redis 7.x 重大新特性与架构升级深度解析
  3. Redis 消息队列深度对比:Pub/Sub、Streams 与 Kafka/RabbitMQ 选型指南