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

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

扩展阅读

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

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