mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1121 字
3 分钟
MVCC 多版本并发控制(MVCC)
2026-06-13

一、为什么需要 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 的写者
mermaid
flowchart 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 写入时创建新版本类似 CoWCoW 在写入时复制被修改的部分;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 });
}
}

六、生产验证#

  • PostgreSQLheapam_visibility.c#L917-L1096HeapTupleSatisfiesMVCC 是核心可见性检查函数,判断堆元组对当前事务是否可见,使用 XidInMVCCSnapshot 检查事务可见性,无需争用 ProcArrayLock
  • etcdkvstore.go#L53-L135store 结构体跟踪 currentRevcompactMainRev,用 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)

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

MVCC 多版本并发控制(MVCC)
https://blog.souloss.com/posts/programming/concurrency/concurrency-mvcc/
作者
Souloss
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时