1121 字
3 分钟
MVCC 多版本并发控制(MVCC)
一、为什么需要 MVCC
数据库里最经典的冲突:读操作和写操作互相阻塞。一个长查询正在扫描全表,所有写操作都得等它读完;反过来,一个写操作正在更新行,所有读操作都得等它提交。在高并发场景下,这种读写互斥是性能杀手。
加读写锁可以缓解,但只是把问题从「互斥」变成了「等锁」。而且读写锁有写饥饿问题——如果读请求源源不断,写锁永远拿不到。
MVCC(Multi-Version Concurrency Control)换了一个思路:为每个值保留多个带时间戳的版本,读者看自己的快照,写者追加新版本,两者互不干扰。读者不阻塞写者,写者不阻塞读者——读写完全并行。这就是 PostgreSQL、MySQL InnoDB、etcd、CockroachDB 等主流存储引擎的选择。
二、现实类比
图书馆保留旧版和新版书。借了第 3 版的读者可以继续读,即使第 4 版已经上架。每个读者看到的是一个一致的快照——没人看到写了一半的更新。旧版没人借了再下架回收。
三、核心思想
MVCC 将每次写入存储为带时间戳或事务 ID 的新版本。读者看到对其快照可见的最新版本,忽略并发写入。
键 "balance"┌──────────┬──────────┬──────────┬──────────┐│ t=100 │ t=200 │ t=300 │ t=400 ││ val=500 │ val=450 │ val=600 │ val=580 │└──────────┴──────────┴──────────┴──────────┘事务 t=250: 看到 val=450 (最新版本 ≤ 250)事务 t=350: 看到 val=600 (最新版本 ≤ 350)两者读取时不阻塞 t=400 的写者mermaidflowchart TD W1[写事务 t=100] --> V1[版本: val=500] W2[写事务 t=200] --> V2[版本: val=450] W3[写事务 t=300] --> V3[版本: val=600] W4[写事务 t=400] --> V4[版本: val=580] R1[读事务 t=250] --> V2 R2[读事务 t=350] --> V3| 属性 | 值 |
|---|---|
| 读写冲突 | 无——读者看快照,写者追加新版本 |
| 写写冲突 | 提交时检测(先写者赢或中止) |
| 空间开销 | 每个键多个版本(通过压缩回收) |
| 隔离级别 | 快照隔离(强于读已提交,弱于可串行化) |
四、变体与对比
| 模式 | 关系 | 区别 |
|---|---|---|
| 写时复制(CoW) | MVCC 写入时创建新版本类似 CoW | CoW 在写入时复制被修改的部分;MVCC 保留完整版本链 |
| 逻辑时钟 | 逻辑时钟提供 MVCC 的版本时间戳 | 逻辑时钟是排序工具;MVCC 是并发控制方案 |
| 墓碑 | MVCC 用墓碑标记已删除版本 | 墓碑是删除标记;MVCC 是版本管理策略 |
| 双缓冲 | 都提供「读者看旧版,写者写新版」 | 双缓冲只有 2 个版本;MVCC 保留多个带时间戳的版本 |
Warning
MVCC 提供的是快照隔离,不是可串行化。两个事务读取相同快照后分别写入不同键,可能产生写偏斜(write skew)——各自通过约束检查,合在一起却违反了约束。需要完全可串行化(如 PostgreSQL 的 SSI)才能防止。
五、多语言实现
Go:多版本键值存储
type Version struct { Timestamp int Value string Deleted bool}
type MVCCStore struct { data map[string][]Version}
func NewMVCCStore() *MVCCStore { return &MVCCStore{data: make(map[string][]Version)}}
func (s *MVCCStore) Put(key, value string, ts int) { s.data[key] = append(s.data[key], Version{Timestamp: ts, Value: value})}
func (s *MVCCStore) Get(key string, ts int) (string, bool) { versions := s.data[key] var best *Version for i := range versions { v := &versions[i] if v.Timestamp <= ts && (best == nil || v.Timestamp > best.Timestamp) { best = v } } if best == nil || best.Deleted { return "", false } return best.Value, true}
func (s *MVCCStore) Delete(key string, ts int) { s.data[key] = append(s.data[key], Version{Timestamp: ts, Deleted: true})}TypeScript:带快照隔离的 MVCC
interface Version<T> { timestamp: number; value: T; deleted: boolean;}
class MVCCStore<T> { private store = new Map<string, Version<T>[]>();
put(key: string, value: T, timestamp: number): void { if (!this.store.has(key)) this.store.set(key, []); this.store.get(key)!.push({ timestamp, value, deleted: false }); }
get(key: string, timestamp: number): T | undefined { const versions = this.store.get(key); if (!versions) return undefined; let best: Version<T> | undefined; for (const v of versions) { if (v.timestamp <= timestamp && (!best || v.timestamp > best.timestamp)) { best = v; } } return best && !best.deleted ? best.value : undefined; }
delete(key: string, timestamp: number): void { if (!this.store.has(key)) this.store.set(key, []); this.store.get(key)!.push({ timestamp, value: undefined as T, deleted: true }); }}六、生产验证
- PostgreSQL — heapam_visibility.c#L917-L1096:
HeapTupleSatisfiesMVCC是核心可见性检查函数,判断堆元组对当前事务是否可见,使用XidInMVCCSnapshot检查事务可见性,无需争用ProcArrayLock - etcd — kvstore.go#L53-L135:
store结构体跟踪currentRev和compactMainRev,用 B 树kvindex进行多版本查找,驱动 Kubernetes 的配置基础设施 - MySQL InnoDB — undo log 实现 MVCC 行版本,每行有
DB_TRX_ID(创建者事务 ID)和DB_ROLL_PTR(指向 undo log 的回滚指针)
七、小结
何时使用:
- 数据库——并发事务的快照隔离(PostgreSQL、MySQL InnoDB)
- 分布式 KV 存储——无分布式锁的一致读(etcd、CockroachDB、TiKV)
- 时间旅行查询——读取过去某时间点的数据
- 乐观并发——提交时检测冲突而非预先加锁
何时不用:
- 单写者系统——只有一个写者时 MVCC 的版本管理开销不必要
- 内存受限——每个键多个版本消耗大量存储
- 只写不读——版本管理开销没有读者收益
- 需要严格可串行化——MVCC 提供快照隔离;完全可串行化需要额外机制(SSI)
八、参考资料
- PostgreSQL MVCC 文档 - PostgreSQL 并发控制详解
- etcd MVCC 实现 - etcd 多版本存储源码
- MySQL InnoDB MVCC - InnoDB 多版本行格式说明
- CockroachDB MVCC - 分布式 SQL 的 MVCC 实现
- 论文: Generalized Isolation Level Definitions - Adya et al., 2000, 隔离级别的形式化定义
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
MVCC 多版本并发控制(MVCC)
https://blog.souloss.com/posts/programming/concurrency/concurrency-mvcc/ 部分信息可能已经过时
相关文章 智能推荐
1
SeqLock 序列锁(SeqLock)
程序设计 序列号标记数据一致性,读优先——内核时钟和系统统计的低开销锁
2
Actor 模型(Actor Model)
程序设计 每个 Actor 独占自己的状态,通过消息通信
3
Channel 通道 / CSP 模型(Channel / CSP)
程序设计 通过通信共享内存,而非共享内存通信——Go 并发模型的根基
4
RCU 读时复制更新(Read-Copy-Update)
程序设计 读无锁、写时复制旧版本延迟释放——Linux 内核链表和路由表热更新的核心机制
5
Thread-Local Storage 线程本地存储(Thread-Local Storage)
程序设计 每个线程私有数据,消除竞争——连接池、随机数生成器的基础机制






