简介
LevelDB 是由 Google 工程师 Jeff Dean 和 Sanjay Ghemawat 开发的开源 KV 存储引擎,两人同时也是 Bigtable 和 MapReduce 的主要设计者。LevelDB 可以理解为单机版的 Bigtable Tablet Server,能够处理十亿级别规模的 Key-Value 数据持久性存储。
主要特性:
- 数据按 Key 有序存储,支持自定义比较函数
- 提供 Put / Get / Delete 以及原子批量操作接口
- 支持 Snapshot 快照读,读操作不受并发写影响
- 支持 Snappy 数据压缩
- 写性能极高:随机写约 40 万条/秒,随机读约 6~10 万条/秒(4核机器)
- 写速度远大于读速度;顺序读写远快于随机读写
LevelDB 是单进程嵌入式库,不提供网络服务,适合作为其他系统的本地存储引擎。Chrome 浏览器用它存储 IndexedDB 数据,比特币/以太坊节点用它存储区块链状态。
LSM-Tree
LevelDB 的核心数据结构是 LSM-Tree(Log-Structured Merge-Tree),一种针对写入密集型场景优化的存储结构。
核心思想: 将随机写转化为顺序写,以牺牲部分读性能换取极高的写入吞吐量。
写入流程
写入请求
↓
WAL(预写日志,顺序写磁盘)
↓
MemTable(内存,有序跳表 SkipList)
↓ 满了之后
Immutable MemTable(只读,等待 flush)
↓ flush(minor compaction)
L0(SSTable 文件,磁盘)
↓ compaction(major compaction)
L1 → L2 → ... Ln
三大放大问题
| 问题 | 说明 |
|---|---|
| 写放大 | 数据会被反复 compaction,实际写磁盘量远大于原始数据量,通常 10x~30x |
| 读放大 | 读时需要依次查 MemTable → Immutable MemTable → L0 → L1 → … |
| 空间放大 | 同一 key 可能存在多个版本,直到 compaction 才合并清理 |
为什么写快
- 所有写入都是顺序 I/O(追加 WAL、批量 flush)
- 没有 B-Tree 那样的随机写和页分裂开销
- 一次写入只涉及一次磁盘顺序追加写 + 一次内存 SkipList 插入
使用 LSM-Tree 的系统
- LevelDB / RocksDB — 经典实现(RocksDB 是 LevelDB 的增强版)
- Cassandra、HBase — 分布式存储
- ClickHouse MergeTree — 列式存储引擎
静态结构
LevelDB 的数据存储在内存和磁盘两部分,主要由六个组件构成:
| 组件 | 位置 | 说明 |
|---|---|---|
| MemTable | 内存 | 当前可读写的有序跳表,最新数据在此 |
| Immutable MemTable | 内存 | 已满、等待 flush 到磁盘的只读 MemTable |
| SSTable (.sst) | 磁盘 | 按层组织的有序 KV 文件,L0~Ln |
| Log (WAL) | 磁盘 | 预写日志,用于崩溃恢复 |
| Manifest | 磁盘 | 记录每个 SSTable 文件属于哪层、Key 范围等元数据 |
| Current | 磁盘 | 记录当前生效的 Manifest 文件名 |
MemTable 与 SkipList
MemTable 内部使用跳表(SkipList) 维护 Key 有序性。SkipList 是平衡树的随机化替代,插入时无需频繁调整节点,写入效率高。Redis 也使用 SkipList 作为有序集合的底层结构。
删除操作在 MemTable 中并不立即删除数据,而是插入一条带删除标记的记录,真正的物理删除在后续 Compaction 时完成(Lazy Delete)。
SSTable 文件结构
所有 .sst 文件内部布局相同,分为数据区和管理区:
- Data Block:存储实际的有序 KV 记录,相邻 Key 使用前缀压缩节省空间
- Meta Block:预留接口(1.2 版本暂未使用)
- Index Block:每个 Data Block 的索引,记录起始位置、大小和最大 Key
- Footer:文件末尾,指向 Index Block 和 Meta Index Block 的位置
L0 的特殊性: L0 的 SSTable 文件之间 Key 范围可能重叠(直接由 MemTable flush 产生);L1 及以上各层内文件 Key 范围不重叠。
Log 文件(WAL)
Log 文件以 32KB 为单位划分物理 Block,每条记录由记录头(CheckSum + 长度 + 类型)和数据组成。记录类型分为 FULL / FIRST / MIDDLE / LAST,用于处理跨 Block 的大记录。
写入流程
对于 Put(Key, Value):
- 将 KV 记录顺序追加到 Log 文件末尾(磁盘顺序写)
- 将 KV 记录插入内存中的 MemTable(SkipList 插入)
写入完成。整个过程只有一次磁盘顺序写 + 一次内存写,速度极快。
对于 Delete(Key):与写入相同,只是写入的是「Key + 删除标记」,不立即删除数据。
读取流程
读取按数据新鲜度从高到低依次查找:
MemTable(最新)
↓ 未找到
Immutable MemTable
↓ 未找到
L0 SSTable(可能多个文件,按新鲜度排序)
↓ 未找到
L1 SSTable(只需查一个文件,Key 不重叠)
↓ 未找到
L2 ... Ln
L0 需要特殊处理: L0 文件 Key 范围可能重叠,需先找出所有包含该 Key 的文件,再按新鲜度依次查找。L1 及以上只需查一个文件。
Cache 加速: LevelDB 维护两级 Cache:
- Table Cache:缓存 SSTable 文件句柄和 Index,避免重复打开文件和读取索引
- Block Cache(可配置):缓存 Data Block 内容,减少磁盘读次数
最优情况下(L0 命中),一次读取只需 2 次磁盘 I/O:读 Index + 读 Block。
Compaction
两种类型
Minor Compaction: 将 Immutable MemTable 按 Key 顺序写入 L0 的新 SSTable 文件。此阶段不做物理删除,删除标记原样写入文件。
Major Compaction: 当某层文件数量或大小超过阈值,选取该层一个文件与下层 Key 范围有重叠的文件进行多路归并排序,生成新的下层文件,同时丢弃无效数据(被新版本覆盖的旧值、已标记删除的记录)。
文件选择采用轮转策略,每次选 Key 范围紧邻上次的文件,保证所有文件都有机会参与合并。
Compaction 期间的磁盘 I/O
Compaction 是 LevelDB 读写放大的主要来源:
读盘: 读取参与合并的所有 SSTable 文件内容(L 层 + L+1 层),一次 major compaction 可能涉及数十个文件。
写盘: 将多路归并排序后的结果写入新的 SSTable 文件,写完后更新 Manifest。
删除: 合并完成后删除旧的 SSTable 文件。
对业务的影响:
- Compaction 和正常读写争抢磁盘 I/O,可能导致写入延迟抖动
- L0 文件积压过多时,LevelDB 会主动限速甚至暂停写入(write stall),等待 compaction 赶上
- 频繁删除数据会触发 Compaction,建议批量删除
RocksDB 的改进:
- 多线程并发 compaction
- Rate limiter 限制 compaction I/O 速率,降低对前台读写的影响
- 支持 Leveled / Universal 等多种 compaction 策略
版本管理
LevelDB 用三个概念管理文件版本:
- Version:保存某一时刻所有 SSTable 文件的快照。一般只有一个 current version,但被 Iterator 引用的旧版本会保持存活,因此 Iterator 用完要及时释放。
- VersionEdit:两个 Version 之间的增量变化(新增/删除了哪些文件),持久化到 Manifest 文件。
- VersionSet:管理所有存活 Version 的集合。
Version0 + VersionEdit → Version1
崩溃恢复时从 Manifest 读取 VersionEdit 重建状态,类似数据库的 redo log。这种设计类似图形学中的双缓冲切换:新版本生成后切换指针,旧版本在没有引用时才释放。
使用场景
持久化缓冲队列(削峰)
LevelDB 可以作为持久化写缓冲使用,实现「一边写入、一边读出」的管道模式:
- 写入端(Producer):将数据以递增的序列号或时间戳作为 Key 写入 LevelDB,得益于 LSM-Tree 架构,写入速度极快(顺序写磁盘),可以吸收突发流量峰值
- 读取端(Consumer):按 Key 顺序扫描读取,处理完毕后删除对应记录,以自身能处理的速率消费数据
- 数据持久化到磁盘:与纯内存队列不同,LevelDB 的数据落盘,进程崩溃后数据不丢失,重启后可继续消费
这种模式可以起到削峰填谷的作用:写入端的流量高峰被 LevelDB 缓冲到磁盘,读取端按匀速消费,避免下游系统因瞬间流量过大而崩溃。
适合此场景的条件
- 数据量超出内存容量,需要落盘缓冲
- 对消息顺序有要求(LevelDB Key 有序)
- 单机嵌入式场景,不想引入 Kafka/RabbitMQ 等重量级中间件
- 可以接受 LevelDB 不提供原生队列语义(需要自行管理消费位点和删除逻辑)
注意事项
- LevelDB 不是消息队列,没有 ACK、重试、多消费者等内置机制,需要自行实现
- 频繁删除已消费数据会触发 Compaction,建议批量删除
- 如果对高吞吐、多消费者、分布式有需求,优先考虑 Kafka 等专用消息队列
其他典型场景
- 本地缓存层:缓存热点数据到本地磁盘,减少远端数据库访问
- 索引存储:存储倒排索引、元数据索引等有序 KV 数据
- 嵌入式数据库:Chrome 浏览器使用 LevelDB 存储 IndexedDB 数据
- 区块链节点:比特币、以太坊节点使用 LevelDB 存储区块链状态数据
Maven 依赖(Java)
<dependency>
<groupId>org.fusesource.leveldbjni</groupId>
<artifactId>leveldbjni-linux64</artifactId>
<version>1.8</version>
</dependency>
<dependency>
<groupId>org.iq80.leveldb</groupId>
<artifactId>leveldb-api</artifactId>
<version>0.12</version>
</dependency>