LevelDB 完全指南 / 第 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 Block | SSTable 的索引块 |
// Table Cache 大小通过 max_open_files 控制
leveldb::Options options;
options.max_open_files = 500; // 默认 1000
Block Cache
| 缓存内容 | 说明 |
|---|
| Data Block | 实际的数据块 |
| Filter Block | Bloom 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 GB | 1-4 GB | 按内存预算 |
| > 10 GB | 4-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 读取 | false | LevelDB 内部自动设置 |
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 内存管理注意事项
各组件内存占用
| 组件 | 内存占用 | 可配置? |
|---|
| MemTable | write_buffer_size × max_write_buffer_number | ✅ |
| Immutable MemTable | 同上(共享上限) | ✅ |
| Block Cache | options.block_cache | ✅ |
| Table Cache | max_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 |
扩展阅读
- LRU Cache 实现:
util/cache.cc - Cache 统计:
include/leveldb/statistics.h - Clock Cache:RocksDB 的替代缓存策略
- 缓存替换算法:LRU vs LFU vs ARC vs TinyLFU
← 第 8 章 · Compaction 机制 | 第 10 章 · Bloom Filter →