MVCC核心约束是读写互不阻塞且读取一致快照:事务启动时获取单调递增时间戳,只读取该时间戳前已提交的版本;写操作追加新版本并原子更新旧版本end_ts,读操作遍历版本链匹配start_ts≤ts

什么是 MVCC 在内存事务中的核心约束
MVCC 不是靠锁排队,而是让读写操作看到“一致的快照”——每个事务启动时,系统必须确定一个逻辑时间戳(如全局递增版本号),并只读取该时间戳之前已提交的版本。关键在于:写不阻塞读,读不阻塞写,且读不到未提交或被覆盖的中间状态。C++ 没有内置事务运行时,所以得自己管理版本链、可见性判断和提交/回滚逻辑。
- 版本不能只存最新值,每个数据项需维护
std::vector或链表形式的版本记录(含start_ts、end_ts、value) - 事务开始时必须获取一个单调递增的
ts(可用std::atomic全局计数器) - 所有读操作必须遍历版本链,找到满足
start_ts ≤ ts 的最新版本(end_ts为MAX_UINT64表示当前活跃)
怎么用 C++ 原生类型实现带版本的原子读写
直接对 int 或 std::string 加锁读写无法支持快照语义。必须把数据封装成可版本化的单元,例如:
struct VersionedValue {
std::atomic<uint64_t> start_ts{0};
std::atomic<uint64_t> end_ts{UINT64_MAX};
std::string value;
<pre class="brush:php;toolbar:false;">bool visible_at(uint64_t ts) const {
return start_ts.load(std::memory_order_acquire) <p>};</p>
-
start_ts和end_ts必须用std::atomic,且读写使用memory_order_acquire/release保证顺序可见性 - 写操作不是覆盖,而是追加新版本,并将前一版本的
end_ts设为当前事务ts(需 CAS 循环确保原子性) - 读操作不能只查头节点,要从最新插入的版本往前扫描(否则可能跳过更早但仍在可见范围内的版本)
事务提交时如何避免写写冲突和脏写
MVCC 本身不解决写写冲突,需要额外的冲突检测机制(如 OCC,Optimistic Concurrency Control)。典型做法是:事务执行阶段只在本地缓存写集(std::unordered_map),提交时再验证所有读过的版本是否仍有效、所有待写键的最新版本是否未被他人更新。
- 提交前调用
validate_read_set():对每个读过的VersionedValue*,检查其start_ts是否仍等于当时读到的值(即没被覆盖) - 写入新版本前,用 CAS 更新旧版本的
end_ts;失败则说明别人已提交,当前事务必须中止重试 - 不要用
std::mutex保护整个写集,那会退化成两阶段锁;CAS + 重试才是符合 MVCC 精神的做法
为什么 std::shared_ptr 不适合直接管理 MVCC 版本链
看起来 std::shared_ptr 能自动管理生命周期,但问题很实际:
- 多个事务可能同时持有指向同一历史版本的
shared_ptr,导致该版本永远无法释放(即使早已不可见) -
shared_ptr的引用计数操作(operator++/operator--)隐含内存屏障,高频版本创建/销毁时性能开销明显 - 更严重的是:版本链的“逻辑删除”(设
end_ts)和“物理回收”必须分离——物理回收只能在确认所有低版本事务都已结束后才能进行,这需要单独的 GC 线程或 epoch-based reclamation(如liburcu风格),不能依赖引用计数
真正可行的做法是:版本对象统一放在预分配的内存池里,用 std::atomic 标记有效性,GC 线程定期扫描所有活跃事务的 start_ts,找出最小存活时间戳,然后批量回收 end_ts 的版本块。
MVCC 的难点不在版本存储,而在可见性判定与垃圾回收的协同——稍有疏忽,要么读到脏数据,要么内存无限增长。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











