裸指针无法表达mvcc版本的生命周期语义,易导致悬空或重复释放;实际系统依赖事务快照、xmin/xmax可见性判断及epoch-based回收等机制保障安全,而非shared_ptr。

为什么不能直接用裸指针管理 MVCC 版本链
裸指针(Version*)本身不带生命周期语义,在并发写入、事务回滚、崩溃恢复等场景下极易悬空或重复释放。数据库系统中 MVCC 版本链(如 PostgreSQL 的 tuple 链、MySQL InnoDB 的 undo log 链)必须支持:多事务并发读取旧版本、后台清理线程安全回收无引用版本、崩溃后能从 WAL 或 checkpoint 恢复一致性状态。裸指针无法自动表达“该版本是否仍被某活跃事务可见”这一关键语义。
实际工程中,所有主流实现都绕过裸指针直接操作,转而依赖:
– 事务快照(Snapshot)记录活跃事务 ID 范围
– 版本头部的事务 ID 字段(xmin/xmax)做可见性判断
– 引用计数或 epoch-based reclamation(如 Linux kernel 的 RCU)延迟释放内存
如何用 std::shared_ptr 模拟 MVCC 版本引用计数
仅在原型验证或嵌入式轻量存储中可考虑用 std::shared_ptr 管理版本对象,但必须满足两个前提:所有访问路径统一走智能指针、禁用裸指针构造/赋值。否则引用计数会失准。
- 每个版本对象需继承自
std::enable_shared_from_this<version></version>,确保从内部获取有效shared_ptr - 事务提交时,将当前写入的版本存入
Transaction::versions_written(std::vector<:shared_ptr>></:shared_ptr>),避免提前析构 - 清理线程调用
Version::is_visible_to_all_active_txns()判断后,再调用reset();注意此时若仍有其他shared_ptr持有该对象,内存不会立即释放
示例关键片段:
struct Version : std::enable_shared_from_this<version> {
txid_t xmin, xmax;
std::string data;
bool is_visible_to(const Snapshot& snap) const {
return snap.contains(xmin) && !snap.contains(xmax);
}
};
// 写入新版本
auto new_ver = std::make_shared<version>(txid, data);
current_txn->versions_written.push_back(new_ver);
</version></version>
真实数据库为何弃用 C++ 智能指针而选 epoch RCU
PostgreSQL 使用 ProcArray + pg_stat_activity 快照跟踪活跃事务;InnoDB 用 trx_sys->rw_trx_list 维护读写事务链表——它们都不依赖 C++ 对象生命周期管理,而是靠全局事务状态机驱动清理。原因很实际:
-
std::shared_ptr的原子计数操作在高并发更新下成为热点,实测比基于 epoch 的批量延迟释放慢 3–5 倍 - 数据库常需跨进程共享内存(如 shared_buffers),
shared_ptr的控制块无法跨进程映射 - 崩溃恢复要求版本对象能从磁盘页重建,而非依赖堆上构造顺序;裸指针+固定偏移访问更可控
如果你正在实现一个需要 crash-safe 的 MVCC 存储引擎,重点应放在:
– 设计可序列化的版本头结构(含 xmin/xmax/ctid)
– 实现基于 WAL 的版本链重链接逻辑(如 PostgreSQL 的 HeapTupleSatisfiesVacuum)
– 用 std::atomic<uint64_t></uint64_t> 维护全局 epoch,配合 hazard pointer 或 EBR 延迟内存回收
清理线程里指针操作最容易踩的坑
即使你坚持用指针,也绝不能在清理线程中直接 delete ptr。典型错误包括:
- 未检查版本是否仍在 buffer pool 中被 pin 住(
BufferDesc->refcount > 0) - 未等待所有使用该版本的后台进程(如 vacuum、backup)完成扫描
- 在持有锁期间执行
delete,导致内存分配器锁与数据库锁嵌套,引发死锁 - 对只读副本误删主库尚未复制的版本(需比对
lsn或commit_ts)
正确做法是两阶段:先标记(置 Version::state = VERSION_DELETED),再异步移交到专用内存池(如 MemoryContext)统一释放;或者走 lock-free 的 hazard pointer 回收路径。
真正棘手的从来不是“怎么删”,而是“怎么确认现在能删”。版本可见性判断和事务状态同步,永远比指针类型选择更消耗心力。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











