介绍
LevelDB 是 Google 开源的高性能键值对嵌入式数据库,具有一系列设计上的优势,特别适合写多读少、对存储空间要求高效的场景。
核心优势
1. 高写入性能(顺序写磁盘)
-
基于 LSM-Tree(Log Structured Merge Tree);
-
所有写操作首先写入内存(memTable)+ WAL;
-
后台异步 flush 到磁盘,避免频繁随机写;
-
大量写入性能远高于 B+Tree 类数据库(如 SQLite)。
2. 数据压缩与空间利用率高
-
支持 Snappy 压缩;
-
自动 Compaction(压缩合并) 机制;
-
清理删除或覆盖的旧版本,回收磁盘空间;
-
多
.sst
层级结构使数据有序紧凑。
3. 支持有序遍历与范围查询
-
支持通过
Iterator
顺序遍历键值; -
可高效进行范围查询(Range Scan):
4. 轻量、嵌入式、依赖少
-
无服务器(serverless),直接嵌入你的应用;
-
单一
.a
静态库或.so
动态库,无需依赖外部组件; -
跨平台支持良好(Linux、macOS、Windows)。
5. Crash-safe 崩溃恢复机制
-
所有写入先写入
.log
文件(Write-Ahead Log); -
崩溃后可自动恢复到一致状态;
-
无需额外事务机制即可保证写入安全性。
6. 支持快照(Snapshot)和原子批处理(WriteBatch)
-
Snapshot
: 提供一致性读取视图; -
WriteBatch
: 批量写入操作可原子提交,提升性能并简化逻辑。
使用场景
场景类型 | 是否适合 | 说明 |
---|---|---|
批量数据写入 | ✅ 非常适合 | 写优化结构(LSM-tree),顺序写性能极高 |
嵌入式/边缘设备存储 | ✅ 非常适合 | 轻量、无服务端,资源开销小 |
日志系统、缓存系统 | ✅ 合适 | 大量写入、偶尔查询 |
用户画像、指标记录 | ✅ 合适 | 小 key-value、高速写入 |
离线分析数据落地 | ✅ 合适 | 批量写入,偶尔按 key 扫描或查询 |
查询很少、但插入频繁的系统 | ✅ 非常合适 | 查询通过前置缓存减少压力 |
不太适合的场景
场景类型 | 原因 |
---|---|
高并发读场景(每秒几千次以上) | 读放大严重,需大量优化(如加 Bloom Filter、前置缓存) |
大量随机读 + 大量随机写 | 写放大 + 查询慢 |
多表 join、事务一致性需求强 | 不支持 SQL 和事务 |
需要按字段查询、复杂查询 | 不支持索引,只有键排序 |
数据结构
-
内存表(MemTable):最新写入的数据,保存在内存中。
-
Immutable MemTable:旧内存表,尚未 flush。
-
SST 文件(磁盘):排序的 Key-Value 存储在多层磁盘文件中。
-
Block Cache(块缓存):LevelDB 会缓存 SST 文件中的数据块(默认 8MB)。
文件结构
- 000123.log:正在写的 WAL
- 000124.sst: L0 的第一个 SST 文件
- 000125.sst: L0 的第二个 SST 文件(写入后产生)
- 000130.sst: L1 的文件(Compaction 后生成)
- MANIFEST-000001:元数据文件
- CURRENT:指向当前 MANIFEST
memTable
它是数据库打开时创建的空内存结构,用于接收写入。
生命周期如下:
-
DB.open()
时,创建一个空的memTable
。 -
写入数据时(
put()
/delete()
),首先写 WAL(Write-Ahead Log),然后写入memTable
。 -
当
memTable
的大小超过阈值(writeBufferSize
)时,会:-
将当前
memTable
移入immutable memTable
; -
异步触发 flush 操作,将其写入
.sst
文件; -
创建新的
memTable
接收写入。
-
触发时机 | 会怎样? |
---|---|
memTable flush | 提高 L0 score |
Compaction 完成 | 降低某层 score,增加下一层 |
写入压力上升 | 多文件生成,score 提高 |
删除数据未及时 compact | score 居高不下,空间占用大 |
compactionScore
是 LevelDB 内部的一个关键指标,它决定是否需要触发 Compaction(压缩),以及优先压缩哪个 Level。
-
每个 Level(L0 ~ L6)都有一个
compactionScore
; -
值越大,代表该 Level 越“拥堵”,越需要被压缩;
-
当
compactionScore ≥ 1.0
,LevelDB 会调度一次 Compaction; -
通常由
VersionSet::Finalize()
计算。
JAVA客户端配置
参数 | 默认值 | 说明 |
---|---|---|
createIfMissing | false | 如果数据库不存在,是否自动创建(通常你会手动设为 true ) |
errorIfExists | false | 如果数据库已存在,是否抛出异常 |
paranoidChecks | false | 是否进行一致性检查 |
verifyChecksums | false | 读取时是否校验数据块的校验和 |
cacheSize | 8 * 1024 * 1024 (8MB) | 内存缓存大小 |
blockSize | 4 * 1024 (4KB) | 每个 block 的大小 |
writeBufferSize | 4 * 1024 * 1024 (4MB) | 写缓冲区大小 |
maxOpenFiles | 1000 | 打开的文件数上限(仅 native JNI 实现中有效) |
compressionType | Snappy | 是否启用 Snappy 压缩 |
使用逻辑
写数据流程
-
写入 WAL(Write-Ahead Log)
-
写入先追加到
.log
文件(顺序写); -
保证宕机后数据可恢复;
-
默认采用 同步写(Sync=true)才能确保持久化;
-
.log
文件位于Level 0
。
-
-
写入 memTable(跳表)
-
同步写入内存结构
memTable
; -
快速写入(无锁 skiplist),数据可被读取;
-
内存中结构,不会持久化;
-
达到
writeBufferSize
限制(默认 4MB)后变为immutable memTable
,进入 flush。
-
-
触发 MemTable Flush,生成 SST 文件
-
将
immutable memTable
转为 SST 文件; -
写入磁盘(SST 为排序存储);
-
这些文件是查询的主要来源(也是 compaction 的输入);
-
会和已有 Level 0 文件产生重叠。
-
-
进行 Compaction(压缩)
-
定期触发(或写压力大时自动触发);
-
将多个 SST 文件合并、去重、合并覆盖;
-
数据从 L0 -> L1 -> L2 逐层下推;
-
保证后期查询高效,数据唯一性提升。
-
举例
执行 db.put("user123", "value1")
,可能流程如下:
-
日志:写入
.log
文件中(WAL); -
内存:写入到
memTable
; -
若 memTable 满:
-
转为 immutable;
-
后台线程 flush 成一个新的
000123.sst
;
-
-
触发 compaction,将多个 sst 合并入更低 level;
-
.log
文件在 flush 成功后被删除。
读取方式
-
先查 memTable(内存表)
-
memTable
是当前活跃写入的跳表结构; -
有序、支持二分查找;
-
如果找到了 key,直接返回对应的 value。
-
-
再查 immutable memTable(只读内存表)
-
memTable
flush 到磁盘前,会被转为 immutable; -
查询会优先在这查找;
-
如果 key 存在,会返回。
-
-
然后查各层 .sst 文件(从 Level 0 开始)
-
Level 0:
-
文件之间的 key 区间可能重叠;
-
必须逐个文件遍历查找;
-
优先查最新的文件(文件编号大 → 数据新);
-
-
Level 1~N(通常到 Level 6):
-
每层中的文件 key 区间互不重叠;
-
可以通过 key 二分定位到最多一个文件;
-
只需要在该文件中查找一次;
-
-
-
使用 Bloom Filter 加速排除(配置生效)
-
每个
.sst
文件都可带一个 Bloom Filter; -
在读文件前先看 Bloom Filter 是否可能包含该 key;
-
否 → 立即跳过;
-
是 → 真正读取磁盘文件;
-
-
可大幅降低磁盘 IO 次数,特别是 key 不存在时。
-
-
读取 Block → 解压缩 → 查找 KV
-
.sst
文件是由多个 Block 组成的; -
使用索引块定位 block;
-
如果有 Snappy 压缩,先解压再查找;
-
查到 key 返回 value,否则继续查下一个层级。
-
举例
执行 db.get("user123")
,可能流程如下:
-
不在 memTable;
-
immutable memTable 也没有;
-
查 L0 中的 3 个文件,(Bloom Filter 排除 2 个),只查 1 个;
-
没找到,再查 L1;
-
在 L1 的某个
.sst
文件中命中 -
去除文件中的block
-
解压 block → 返回 value。
重启恢复步骤
-
读取
.log
文件; -
重建 memTable;
-
保证数据一致性;
-
再继续写入。