3. 键值存储选型
## 3. 键值存储选型:LevelDB、RocksDB、SQLite 作为键值引擎的读写模型与适用场景 ### 3.1 边界与分层定位 在离线缓存方案中,键值引擎承担**本地持久化**与**单机高吞吐读写**职责。它处于缓存架构的底层,向上为缓存管理器提供 Put/Get/Delete 接口,向下直接管理磁盘文件与内存缓冲区。 | 引擎 | 定位 | 典型使用层 | |------|------|------------| | **LevelDB** | 单线程写入、高压缩比的嵌入式 LSM-Tree 引擎 | 轻量离线缓存、日志型存储、冷热数据分离的冷层 | | **RocksDB** | 多线程并发、支持列族与多级压缩的优化版 LSM-Tree | 高并发缓存、流式处理、需要 RocksDB 专属优化(如 BlobDB)的场景 | | **SQLite** | 关系型行存引擎,支持 SQL 查询 | 需要索引、事务及结构化查询的混合型缓存(如配置缓存 + 键值缓存合并) | 三者均工作在**进程内嵌入模式**(无独立进程),通过本地文件系统直接写入磁盘,无网络开销。 ### 3.2 组件职责对比 每个引擎的内部组件决定了它的读写路径与性能特征。 | 组件 / 概念 | LevelDB | RocksDB | SQLite | |-------------|---------|---------|--------| | 写入缓冲 | 单一 MemTable(跳表) | 多 MemTable,可配置 WriteBufferManager | 页面缓存 pager,默认 2 MB | | 写入日志 | WAL(Write-Ahead Log),追加写 | WAL(可配置 Group Commit) | WAL 或 Rollback Journal | | 持久化结构 | SSTable 文件(分层 L0→L…) | SSTable + Blob 文件(分离大值) | 单一 .db 文件(B-tree 页) | | 压缩 | Snappy(默认),支持自定义 | Snappy / ZSTD / LZ4 / 自定义 | 无内置压缩,可通过 VFS 扩展 | | 并发控制 | 单 Writer + 多个并行 Reader | 多 Writer(通过 Column Family 或合并器释放并发) | 多 Reader 单 Writer(WAL 模式允许多并发读取) | | 缓存层 | TableCache(只读 SST 块缓存) | BlockCache(分片 LRU)+ RowCache | 页面缓存 + 可选 mmap | ### 3.3 数据流与控制流 以下以 **Get(key)** 与 **Put(key, value)** 为例,展示三个引擎的典型读写路径。
**读写流共性**:所有引擎均使用 WAL 确保 crash-safe,但刷盘时机、压缩触发、缓存穿透策略差异显著,直接影响写放大与读延迟。
#### 3.3.1 LevelDB 读写路径
- **Put** → 追加写入当前 WAL → 写入 MemTable(有序跳表) → 当 MemTable 满时切为 Immutable MemTable → 后台线程 compact 到 L0 SSTable → 逐层合并。
- **Get** → 先查 MemTable → 再查 Immutable → 最后按层扫描 SSTable(需二分查找索引块)。
#### 3.3.2 RocksDB 读写路径
- **Put** → 写入 WAL(可配置 batch 提交) → 写入当前 MemTable(支持多 MemTable 并行) → 后台 compaction 线程将 SSTable 从 L0 合并到更下层。
支持 **BlobDB**:大值(> 阈值)直接写入 Blob 文件,SSTable 仅保留指针。
- **Get** → 优先查 MemTable / Immutable → 查 BlockCache(已解压块) → 查 RowCache(行缓存) → 逐层读 SSTable(极细粒度过滤:BloomFilter + Index 块)。
#### 3.3.3 SQLite 作为键值引擎的路径
当以 `CREATE TABLE kv (key TEXT PRIMARY KEY, value BLOB)` 模拟键值存储时:
- **Put**(`INSERT OR REPLACE`)→ 写入 WAL → 更新 B-tree 页面 → 页面刷新到磁盘。
- **Get**(`SELECT value FROM kv WHERE key=?`)→ 查页面缓存 → 未命中则从 B-tree 做二分查找 → 返回到应用层。
**信任链路**:应用直接调用引擎 API,引擎通过操作系统文件 I/O(同步/异步)持久化。RocksDB 允许设置 `Direct I/O` 绕过页缓存;LevelDB 依赖 OS 缓存;SQLite 通过 mmap 或常规读写控制。
### 3.4 选型权衡表
| 维度 | LevelDB | RocksDB | SQLite |
|------|---------|---------|--------|
| **写吞吐**(单线程) | 高(CPU 密集少,适配小值) | 极高(多线程、列族、BlobDB 分离大值) | 中(受 SQL 解析与 B-tree 插入开销限制) |
| **读延迟**(随机) | 低(层次少 + Bloom 过滤) | 极低(三级缓存 + 多级过滤) | 中(需 B-tree 完整搜索,缓存命中率依赖配置) |
| **空间放大**(Write Amplification) | 中(Level 压缩默认 ~10x) | 低到中(可调 Tiered / Level / Universal 压缩) | 极低(B-tree 原地更新,仅 WAL 带来少量放大) |
| **并发模型** | 单 Writer | 多 Writer + 多个 Compaction 线程 | 单 Writer(WAL 模式允许并行读) |
| **Schema 支持** | 无 | 无(但有 Column Family 逻辑分区) | 完整 SQL + 二级索引 + 触发器 |
| **文件管理** | 多个小文件(目录 + log + sst) | 同 LevelDB 但可配置 Blob 文件 | 单个 .db 文件 + WAL 文件 |
| **跨平台与嵌入** | 极好(纯 C++,少量依赖) | 好(可选 RocksDB 版本,需注意 C++17) | 极好(单一 C 库,支持内存 DB) |
### 3.5 风险点
**1. 写放大风险**:LevelDB 与 RocksDB 的 compaction 策略若配置不当,写放大可达 30-50x,导致 SSD 寿命快速消耗。必须结合实际数据更新频率与 TTL 调整压缩类型。
**2. 文件数上限**:LevelDB 与 RocksDB 的 SSTable 文件数量随写入量增长,过多文件(> 10万)会触发压缩风暴。需监控 L0 文件数并调优 `max_background_compactions`。
**3. SQLite 的事务隔离**:在重复 INSERT OR REPLACE 场景下,若未使用 `BEGIN ... COMMIT` 包裹,每次 Put 都会自动提交,磁盘同步次数剧增。必须启用 WAL 模式并批量提交。
### 3.6 落地步骤
1. **确定基线**:采集离线缓存业务的写 / 读 QPS、值大小分布(均值、P99)、数据有效期。
2. **粗筛引擎**:
- 写 QPS > 50k/s 且值 < 1KB → 优先 Roc skDB。
- 写 QPS < 5k/s 且需要 SQL 查询 → SQLite。
- 写 QPS 中等,对空间不敏感 → LevelDB。
3. **微基准测试**:使用 `db_bench`(RocksDB)或 `leveldb-bench` 模拟实际写入模式(随机 key,连续 value 大小分布)。
4. **集成测试**:在离线缓存架构中替换引擎层,验证端到端延迟与内存占用。
5. **配置调优**:
- RocksDB:调节 `block_size`(默认 4KB 调至 16KB),开启 `compaction_readahead_size`,设置合适的 `max_bytes_for_level_multiplier`。
- SQLite:`PRAGMA journal_mode=WAL;` `PRAGMA synchronous=NORMAL;` `PRAGMA cache_size=-8000;`。
- LevelDB:控制 `max_file_size`(默认 2MB),开启压缩,调节 `write_buffer_size`。
### 3.7 验收标准
| 检查项 | 通过条件 |
|--------|----------|
| 随机写入吞吐 | 选定引擎在目标值大小下稳定达到业务峰值 QPS,且 P99 写入时延 < 5 ms |
| 随机读取延迟 | P99 读取延迟 < 3 ms(从缓存命中到返回) |
| 磁盘写入量(Write Amplification) | 在稳态下,物理写入量 / 逻辑写入量 < 3 倍 |
| 文件数稳定性 | 文件总数在 72h 运行后增长不超过 5% |
| 崩溃恢复 | 在随机写入过程中 kill 进程,重启后不丢已持久化的数据(通过 WAL 校验) |
---
**2. 文件数上限**:LevelDB 与 RocksDB 的 SSTable 文件数量随写入量增长,过多文件(> 10万)会触发压缩风暴。需监控 L0 文件数并调优 `max_background_compactions`。
**3. SQLite 的事务隔离**:在重复 INSERT OR REPLACE 场景下,若未使用 `BEGIN ... COMMIT` 包裹,每次 Put 都会自动提交,磁盘同步次数剧增。必须启用 WAL 模式并批量提交。
**彩色讲义提示**:选择引擎不是终点 – 同一引擎在不同配置下的性能差异可达 10 倍。务必在最终硬件上使用业务数据分布做压力测试,而不是依赖公开基准。
### **三个引擎的内部读写结构对比**
> 上图将三种引擎的写入链路、缓存层次和持久化结构并行对比,便于直观看出:LevelDB 最精简,RocksDB 在缓存与并行度上最丰富,SQLite 则具有 SQL 栈与 B-tree 有序结构带来的不同读写特性。