强曰为道

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

第 9 章 · Block Cache

第 9 章 · Block Cache

9.1 为什么需要 Block Cache

LevelDB 的数据最终存储在磁盘上的 SSTable 文件中。每次读取都访问磁盘会非常慢:

没有 Block Cache 的读取路径:

  Get("user:1001:name")
    → 查 MemTable(内存,~微秒)
    → 查 Immutable MemTable(内存,~微秒)
    → 查 Level 0 SSTable(磁盘 I/O,~毫秒)  ← 慢!
    → 查 Level 1 SSTable(磁盘 I/O,~毫秒)  ← 慢!
    ...

每次读取可能触发多次磁盘 I/O,延迟在毫秒级。

Block Cache 将访问过的 SSTable 数据块缓存在内存中,避免重复的磁盘读取:

有 Block Cache 的读取路径:

  Get("user:1001:name")
    → 查 MemTable(未命中)
    → 查 Block Cache → 命中!直接返回(~微秒)
    
  命中率高时,大部分读取不需要访问磁盘。

9.2 LRU Cache 原理

LevelDB 使用 LRU(Least Recently Used) 策略管理缓存。

LRU 工作原理

缓存容量:4 个块

访问序列:A → B → C → D → A → E

步骤 1: [A]           访问 A,缓存未命中,加载 A
步骤 2: [A, B]        访问 B,缓存未命中,加载 B
步骤 3: [A, B, C]     访问 C,缓存未命中,加载 C
步骤 4: [A, B, C, D]  访问 D,缓存未命中,加载 D
步骤 5: [B, C, D, A]  访问 A,缓存命中,将 A 移到最前
步骤 6: [C, D, A, E]  访问 E,缓存未命中,淘汰最久未用的 B

LRU 链表:
  最近使用 ←→ ... ←→ 最久未使用
  (Head)              (Tail)
  命中时移到 Head,淘汰时从 Tail 移除

LRU Cache 数据结构

// LevelDB 内部的 LRU Cache 实现(简化版)
struct LRUHandle {
    void* value;
    void (*deleter)(const Slice&, void* value);
    LRUHandle* next_hash;   // 哈希表链
    LRUHandle* next;         // LRU 双向链表
    LRUHandle* prev;
    size_t charge;
    uint32_t hash;
    char key_data[1];        // 变长 key
};

struct LRUCache {
    size_t capacity;          // 缓存容量
    std::unordered_map<uint32_t, LRUHandle*> table;  // 哈希表
    LRUHandle lru;            // LRU 链表头
    size_t usage;             // 当前使用量
};

9.3 两级缓存架构

LevelDB 实际上有两级缓存

┌──────────────────────────────────────────────┐
│                读取请求                        │
└────────────────────┬─────────────────────────┘
                     │
                     ▼
┌──────────────────────────────────────────────┐
│           Table Cache (文件级缓存)             │
│  缓存已打开的 SSTable 文件句柄 + Index Block   │
│  大小:max_open_files 控制                     │
└────────────────────┬─────────────────────────┘
                     │
                     ▼
┌──────────────────────────────────────────────┐
│           Block Cache (数据块缓存)             │
│  缓存 Data Block 和 Filter Block              │
│  大小:options.block_cache 控制               │
└──────────────────────────────────────────────┘

Table Cache

缓存内容说明
文件句柄已打开的 SSTable fd
Table 对象解析后的 SSTable 元数据
Index BlockSSTable 的索引块
// Table Cache 大小通过 max_open_files 控制
leveldb::Options options;
options.max_open_files = 500;  // 默认 1000

Block Cache

缓存内容说明
Data Block实际的数据块
Filter BlockBloom Filter 块
Uncompression Dict压缩字典(如果使用)
// Block Cache 大小
leveldb::Options options;
options.block_cache = leveldb::NewLRUCache(128 * 1024 * 1024);  // 128MB

9.4 配置 Block Cache

设置缓存大小

#include "leveldb/cache.h"
#include "leveldb/db.h"

leveldb::Options options;

// 方法 1:创建自定义大小的 LRU Cache
options.block_cache = leveldb::NewLRUCache(256 * 1024 * 1024);  // 256MB

// 方法 2:禁用 Block Cache(每次读磁盘)
options.block_cache = leveldb::NewLRUCache(0);

// 方法 3:共享多个数据库实例的缓存
auto shared_cache = leveldb::NewLRUCache(512 * 1024 * 1024);  // 512MB
options1.block_cache = shared_cache;
options2.block_cache = shared_cache;

缓存大小选择指南

数据集大小推荐 Block Cache说明
< 100 MB数据集大小全部缓存
100 MB - 1 GB数据集的 50%缓存热数据
1 GB - 10 GB1-4 GB按内存预算
> 10 GB4-16 GB考虑 SSD 作为后备
> 100 GB不适用 LevelDB考虑 RocksDB

9.5 缓存与读取选项

fill_cache 选项

// 场景 1:正常读取(填充缓存)
leveldb::ReadOptions ropts;
ropts.fill_cache = true;  // 默认
db->Get(ropts, "hot_key", &value);  // 读取结果加入缓存

// 场景 2:批量扫描(不污染缓存)
leveldb::ReadOptions ropts;
ropts.fill_cache = false;
auto* it = db->NewIterator(ropts);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
    // 大量顺序读取不会把热数据挤出缓存
}
delete it;

最佳实践

场景fill_cache说明
点查询(热数据)true命中后留在缓存
批量扫描false不污染缓存
数据迁移false一次性读取
数据校验false不影响正常缓存
Compaction 读取falseLevelDB 内部自动设置

9.6 缓存性能分析

命中率计算

leveldb::Options options;
options.statistics = leveldb::CreateDBStatistics();

// ... 使用数据库 ...

auto stats = options.statistics;
uint64_t hits = stats->getTickerCount(leveldb::BLOCK_CACHE_HIT);
uint64_t misses = stats->getTickerCount(leveldb::BLOCK_CACHE_MISS);
double hit_rate = (double)hits / (hits + misses) * 100.0;

std::cout << "Block Cache 命中率: " << hit_rate << "%" << std::endl;

命中率目标

命中率评价建议
> 95%优秀维持当前配置
80% - 95%良好考虑增大缓存
60% - 80%一般增大缓存或优化访问模式
< 60%必须增大缓存或检查访问模式

9.7 业务场景

场景一:Web API 缓存层

class CachedUserAPI {
public:
    CachedUserAPI(leveldb::DB* db) : db_(db) {
        // 配置较大的 Block Cache
        leveldb::Options opts;
        opts.block_cache = leveldb::NewLRUCache(512 * 1024 * 1024);
        // ... 重新打开数据库 ...
    }

    // 热点用户查询(会被缓存)
    std::string GetUser(int user_id) {
        std::string value;
        leveldb::ReadOptions ropts;
        ropts.fill_cache = true;
        db_->Get(ropts, "user:" + std::to_string(user_id), &value);
        return value;
    }

    // 导出所有用户(不污染缓存)
    void ExportAllUsers(std::ostream& out) {
        leveldb::ReadOptions ropts;
        ropts.fill_cache = false;
        auto* it = db_->NewIterator(ropts);
        for (it->Seek("user:"); it->Valid(); it->Next()) {
            if (!it->key().starts_with("user:")) break;
            out << it->key().ToString() << "\t"
                << it->value().ToString() << "\n";
        }
        delete it;
    }

private:
    leveldb::DB* db_;
};

场景二:多数据库共享缓存

// 配置服务:用户数据库 + 配置数据库共享缓存
auto shared_cache = leveldb::NewLRUCache(1024 * 1024 * 1024);  // 1GB

leveldb::Options user_opts;
user_opts.create_if_missing = true;
user_opts.block_cache = shared_cache;
leveldb::DB* user_db;
leveldb::DB::Open(user_opts, "/data/users", &user_db);

leveldb::Options config_opts;
config_opts.create_if_missing = true;
config_opts.block_cache = shared_cache;
leveldb::DB* config_db;
leveldb::DB::Open(config_opts, "/data/config", &config_db);

// 两个数据库公平竞争缓存空间

9.8 内存管理注意事项

各组件内存占用

组件内存占用可配置?
MemTablewrite_buffer_size × max_write_buffer_number
Immutable MemTable同上(共享上限)
Block Cacheoptions.block_cache
Table Cachemax_open_files × ~10KB
Bloom Filter每个 key 约 10 bits
WAL Buffer约 32KB
索引结构取决于 SSTable 数量

内存预算计算

假设:
  write_buffer_size = 64MB
  max_write_buffer_number = 4
  block_cache = 512MB
  max_open_files = 1000
  数据库大小 = 10GB
  Bloom Filter = 10 bits/key
  key 数量 = 1000万

内存预算:
  MemTable: 64MB × 2 = 128MB(通常只有 1-2 个活跃)
  Block Cache: 512MB
  Table Cache: 1000 × 10KB ≈ 10MB
  Bloom Filter: 10M × 10 bits ≈ 12MB
  ─────────────────────────────
  总计: ~662MB

9.9 性能调优技巧

技巧一:增大 Block Size 减少缓存条目数

// 较大的 block_size 减少索引条目,但可能降低缓存效率
options.block_size = 8192;  // 8KB(默认 4KB)

技巧二:区分热数据和冷数据

// 将热数据和冷数据存储在不同的数据库实例中
// 热数据库:大缓存
leveldb::Options hot_opts;
hot_opts.block_cache = leveldb::NewLRUCache(1GB);

// 冷数据库:小缓存
leveldb::Options cold_opts;
cold_opts.block_cache = leveldb::NewLRUCache(64MB);

技巧三:监控并调整

// 定期检查缓存命中率,动态调整
void MonitorCache(leveldb::DB* db, leveldb::Statistics* stats) {
    uint64_t hits = stats->getTickerCount(leveldb::BLOCK_CACHE_HIT);
    uint64_t misses = stats->getTickerCount(leveldb::BLOCK_CACHE_MISS);
    double rate = (double)hits / (hits + misses);

    if (rate < 0.8) {
        // 缓存命中率低,考虑增大缓存或优化数据访问模式
        LOG(WARNING) << "Block Cache hit rate low: " << rate;
    }
}

9.10 本章小结

要点内容
LRU Cache最近最少使用策略,缓存 SSTable 数据块
两级缓存Table Cache(文件句柄)+ Block Cache(数据块)
关键配置block_cache 大小、fill_cache 选项、max_open_files
命中率目标 > 90%,通过 Statistics 监控
内存预算MemTable + Block Cache + Table Cache + Bloom Filter

扩展阅读

  1. LRU Cache 实现util/cache.cc
  2. Cache 统计include/leveldb/statistics.h
  3. Clock Cache:RocksDB 的替代缓存策略
  4. 缓存替换算法:LRU vs LFU vs ARC vs TinyLFU

第 8 章 · Compaction 机制 | 第 10 章 · Bloom Filter