LevelDB 完全指南 / 第 7 章 · 快照与 MVCC
第 7 章 · 快照与 MVCC
7.1 什么是 Snapshot
Snapshot(快照)是 LevelDB 提供的一个时间点一致性视图。一旦获取了某个 Snapshot,后续的所有写入都不会影响在该 Snapshot 上的读取结果。
时间线:
T1: Put("key1", "value_v1")
T2: snap = GetSnapshot() ← 获取快照
T3: Put("key1", "value_v2") ← 修改数据
T4: Get("key1") → 返回 "value_v2"(最新值)
T4: Get(snap, "key1") → 返回 "value_v1"(快照值)
7.2 基本用法
C++ 示例
#include "leveldb/db.h"
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::DB::Open(options, "/tmp/snapdb", &db);
// 写入初始数据
db->Put(leveldb::WriteOptions(), "counter", "100");
// 获取快照
const leveldb::Snapshot* snap = db->GetSnapshot();
// 修改数据
db->Put(leveldb::WriteOptions(), "counter", "200");
// 在快照上读取(得到旧值)
leveldb::ReadOptions ropts;
ropts.snapshot = snap;
std::string value;
db->Get(ropts, "counter", &value);
std::cout << "快照值: " << value << std::endl; // 输出: 100
// 普通读取(得到新值)
db->Get(leveldb::ReadOptions(), "counter", &value);
std::cout << "当前值: " << value << std::endl; // 输出: 200
// 释放快照(必须!)
db->ReleaseSnapshot(snap);
delete db;
Go 示例
// 获取快照
snap, _ := db.GetSnapshot()
defer snap.Release()
// 在快照上读取
val, _ := snap.Get([]byte("counter"), nil)
fmt.Printf("快照值: %s\n", val)
Python 示例
snap = db.snapshot()
# 修改数据
db.put(b'counter', b'200')
# 快照读取旧值
old_val = snap.get(b'counter') # 仍为 b'100'
7.3 MVCC 机制
LevelDB 使用 MVCC(Multi-Version Concurrency Control) 来实现 Snapshot。
Sequence Number
每个写入操作都会分配一个递增的 Sequence Number:
操作序列:
Seq=1: Put("name", "张三")
Seq=2: Put("age", "30")
Seq=3: Delete("age")
Seq=4: Put("name", "李四")
Seq=5: Put("email", "[email protected]")
InternalKey 格式
┌──────────────────────────────────────────────┐
│ user_key │ sequence_number │ type │
│ (变长) │ (7 字节) │ (1 字节) │
└──────────────────────────────────────────────┘
Sequence Number 的作用:
- 每条记录都有唯一的 Sequence Number
- 读取时,只能看到 Sequence Number <= Snapshot 的记录
- 这就是 MVCC 的核心:同一 Key 的多个版本共存
读取时的版本选择
MemTable 中的数据:
Key="name", Seq=1, Type=Put, Value="张三"
Key="name", Seq=4, Type=Put, Value="李四"
Snapshot 在 Seq=2 获取:
读取 "name" → 找到 Seq=4 的记录,但 4 > 2,跳过
→ 找到 Seq=1 的记录,1 <= 2,返回 "张三"
无 Snapshot 读取:
读取 "name" → 找到 Seq=4 的记录,返回 "李四"
7.4 Snapshot 与 Iterator
Snapshot 也可以用于 Iterator,保证遍历的一致性:
const leveldb::Snapshot* snap = db->GetSnapshot();
// T1: 数据库有 3 条记录
// T2: 开始遍历
leveldb::ReadOptions ropts;
ropts.snapshot = snap;
leveldb::Iterator* it = db->NewIterator(ropts);
// T3: 其他线程写入新数据
// (新数据的 Seq > snap 的 Seq)
// T4: 继续遍历,新数据不可见
for (it->SeekToFirst(); it->Valid(); it->Next()) {
// 只看到 T1 时刻的数据
}
delete it;
db->ReleaseSnapshot(snap);
7.5 快照的生命周期管理
基本规则
| 规则 | 说明 |
|---|---|
| 必须显式释放 | ReleaseSnapshot(snap) |
| 不能释放两次 | 会导致未定义行为 |
| 释放后不能使用 | 会导致未定义行为 |
| 快照阻止 Compaction 清理旧数据 | 长时间持有快照会增加空间占用 |
RAII 封装
class ScopedSnapshot {
public:
explicit ScopedSnapshot(leveldb::DB* db)
: db_(db), snapshot_(db->GetSnapshot()) {}
~ScopedSnapshot() {
if (snapshot_) {
db_->ReleaseSnapshot(snapshot_);
}
}
const leveldb::Snapshot* Get() const { return snapshot_; }
// 禁止复制
ScopedSnapshot(const ScopedSnapshot&) = delete;
ScopedSnapshot& operator=(const ScopedSnapshot&) = delete;
// 允许移动
ScopedSnapshot(ScopedSnapshot&& other) noexcept
: db_(other.db_), snapshot_(other.snapshot_) {
other.snapshot_ = nullptr;
}
private:
leveldb::DB* db_;
const leveldb::Snapshot* snapshot_;
};
// 使用
void ReadConsistent(leveldb::DB* db) {
ScopedSnapshot snap(db);
leveldb::ReadOptions ropts;
ropts.snapshot = snap.Get();
// ... 使用 ropts 读取
} // 自动释放快照
7.6 业务场景
场景一:一致性备份
bool BackupDatabase(leveldb::DB* db, const std::string& backup_path) {
// 1. 获取一致性快照
const leveldb::Snapshot* snap = db->GetSnapshot();
// 2. 基于快照遍历所有数据
leveldb::ReadOptions ropts;
ropts.snapshot = snap;
leveldb::Iterator* it = db->NewIterator(ropts);
// 3. 写入备份文件
std::ofstream out(backup_path, std::ios::binary);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
std::string key = it->key().ToString();
std::string value = it->value().ToString();
uint32_t klen = key.size();
uint32_t vlen = value.size();
out.write(reinterpret_cast<char*>(&klen), 4);
out.write(key.data(), klen);
out.write(reinterpret_cast<char*>(&vlen), 4);
out.write(value.data(), vlen);
}
delete it;
db->ReleaseSnapshot(snap);
return true;
}
场景二:读取期间的一致性查询
// 生成报表:需要读取多个 Key,期间数据不能变化
Report GenerateReport(leveldb::DB* db) {
ScopedSnapshot snap(db);
leveldb::ReadOptions ropts;
ropts.snapshot = snap.Get();
Report report;
std::string val;
db->Get(ropts, "stats:total_orders", &val);
report.total_orders = std::stoll(val);
db->Get(ropts, "stats:total_revenue", &val);
report.total_revenue = std::stod(val);
db->Get(ropts, "stats:active_users", &val);
report.active_users = std::stoi(val);
return report;
// 快照保证三个数据来自同一时刻
}
场景三:Change Data Capture (CDC)
// 记录上次读取的位置,增量获取变化
class ChangeReader {
public:
ChangeReader(leveldb::DB* db, const std::string& checkpoint_key)
: db_(db), checkpoint_key_(checkpoint_key) {
// 读取上次的 Sequence Number
std::string val;
db->Get(leveldb::ReadOptions(), checkpoint_key, &val);
last_seq_ = val.empty() ? 0 : std::stoull(val);
}
std::vector<Change> ReadChanges() {
std::vector<Change> changes;
uint64_t current_seq = last_seq_;
const leveldb::Snapshot* snap = db_->GetSnapshot();
leveldb::ReadOptions ropts;
ropts.snapshot = snap;
leveldb::Iterator* it = db_->NewIterator(ropts);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
// 需要自定义比较器来过滤 Sequence Number
// 这里简化处理
changes.push_back({
it->key().ToString(),
it->value().ToString()
});
}
delete it;
db_->ReleaseSnapshot(snap);
return changes;
}
private:
leveldb::DB* db_;
std::string checkpoint_key_;
uint64_t last_seq_;
};
7.7 快照的代价
| 维度 | 影响 |
|---|---|
| 内存 | 快照本身只占一个 Sequence Number(极小) |
| 磁盘空间 | 快照阻止 Compaction 清理旧版本数据 |
| 性能 | 迭代器需要跳过新版本记录,有一定开销 |
空间占用示例
假设:
每秒写入 1000 条记录,每条 1KB
保留快照 24 小时
空间占用 = 1000 × 1KB × 86400 ≈ 82 GB 额外空间
→ 不要长时间持有快照!
⚠️ 注意:长时间持有的 Snapshot 会阻止 Compaction 清理旧数据,导致磁盘空间不断增长。使用完毕后应立即释放。
7.8 Snapshot vs ReadOptions 对比
| 特性 | 普通读取 | Snapshot 读取 |
|---|---|---|
| 一致性 | 读取调用时刻的最新值 | Snapshot 创建时刻的值 |
| 多 Key 一致性 | 无保证(每次读取可能不同) | 保证(所有读取基于同一时刻) |
| 性能 | 最快 | 略慢(需要比较 Sequence Number) |
| 内存开销 | 无 | 极小(一个 Sequence Number) |
| 磁盘影响 | 无 | 阻止旧数据清理 |
7.9 注意事项
| ⚠️ 警告 | 说明 |
|---|---|
| 快照必须释放 | GetSnapshot() 和 ReleaseSnapshot() 必须配对 |
| 不可跨进程 | Snapshot 只在当前进程内有效 |
| 不可序列化 | Snapshot 不能保存到磁盘或发送到其他节点 |
| Compaction 影响 | 活跃快照会阻止数据清理 |
7.10 本章小结
| 要点 | 内容 |
|---|---|
| Snapshot | 时间点一致性视图,基于 Sequence Number |
| MVCC | 同一 Key 的多个版本共存,按 Sequence Number 选择 |
| 使用场景 | 一致性备份、报表生成、Change Data Capture |
| 注意事项 | 及时释放、避免长时间持有、影响 Compaction |
扩展阅读
- Snapshot 实现:
db/snapshot.h - MVCC 原理:PostgreSQL 的 MVCC 实现对比
- Sequence Number:
db/dbformat.h,理解 InternalKey 编码