强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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", "li@example.com")

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

扩展阅读

  1. Snapshot 实现db/snapshot.h
  2. MVCC 原理:PostgreSQL 的 MVCC 实现对比
  3. Sequence Numberdb/dbformat.h,理解 InternalKey 编码

第 6 章 · 自定义比较器 | 第 8 章 · Compaction 机制