简介
PalDB 是 Linkedin 公司开源的一款只读型的 KV 存储数据库,目的是在某些场景下替代 HashMap/HashSet 或 LevelDB,在性能和内存之间做了一个良好的平衡。下面是官方给出的测试图表:
使用方式
作为一个存储工具包,其使用方式也很简单,一看就会明白:
//写数据
StoreWriter writer = PalDB.createWriter(new File("store.paldb"));
writer.put("foo", "bar");
writer.put(1213, new int[] {1, 2, 3});
writer.close();
//读数据
StoreReader reader = PalDB.createReader(new File("store.paldb"));
String val1 = reader.get("foo");
int[] val2 = reader.get(1213);
reader.close();
应用场景
PalDB 适合一次写入,多次读取,且数据量较大的场景,如:
- Hadoop/Spark 计算时产生的一些中间结果
- 机器学习训练出的模型
- 词典
实现原理
PalDB 本质上是一个哈希表,用开放寻址法处理哈希冲突。下面从读写两方面来分析其实现细节。
写
写数据的过程主要分为3块:序列化,预写入,最终写入。
序列化:序列化过程主要负责将准备写入的 key-value 值进行序列化。PalDB 自己实现了对 java 基本对象的序列化,对数据进行了一定的压缩(如果觉得压缩的仍不够,PalDB 默认支持 Snappy 压缩算法,可手动开启)。
预写入:程序每调用一次 writer.put(Object key, Object value) ,PalDB 就进行一次预写入。预写入负责写两类文件:
- 索引文件:存储 key 以及 value 在数据文件中的位置
- 数据文件:存储 value 长度以及 value
这两类文件都有一个或者多个,成对出现,文件数量决于 key(序列化后)的长度,一个 key 长度对应一对<索引文件,数据文件>。也就是说,key 长度是一个一级索引,这个在读的时候会用到。下面用一张图总结下预写入的过程。
预写入过程中还会记录一些重要的值,如:value 位置的最大长度,key 总数以及每个 key 长度下的 key 数量。
3.最终写入
当写完数据最终调用 writer.close() 时就进入最终写入阶段。预写入生成的索引文件只是顺序的存储了 key 以及 value 在数据文件中的位置,最终写入阶段负责将索引文件转化成哈希表,跟索引文件一样,每一个 key 长度对应一个哈希表。对每一个哈希表:
- 哈希表 slot 数量 = 该 key 长度下的 key 数量 / loadFactor(默认0.75,可手动指定)
- 每个 slot 的大小是固定的,等于 key 长度 + value 位置的最大长度(因此,slot 里的数据其实是有部分空闲的)。
写这个哈希表的过程是顺序读预写入阶段生成的索引文件,按 key hash 到指定 slot(用开放寻址法处理哈希冲突)并写入 key 以及 value 位置的过程。
遍历处理完所有 key 长度对应的索引文件后,将所有哈希表、数据文件、meta 信息拼接,形成最终的数据库文件。文件结构如下:
读
首先 PalDB 会将数据库文件初始化,初始化过程分为三步:
- 读取 meta 信息,如 key 数据,key 长度数量、每个 key 长度对应的索引文件 slot 数等,并存储在内存中。
- 以一个只读内存映射文件方式(MappedByteBuffer)打开 key 索引集合。由于 PalDB 将 key 索引集合当做一个文件打开,由于内存映射文件的大小限制,key 索引集合的大小不能超过 2G。
- 以一个或多个只读内存映射文件方式打开数据文件集合。如果数据文件过大(大于2G),PalDB 会将其切分成多块。
初始化完成后,就可以调用 reader.get(Object o) 方法进行数据读取,数据读取的流程如下:
总结
PalDB 的实现原理还是比较简单的,但是在某些场景下效果会比常规方法更好。就笔者的实践来说,用 PalDB 存储推荐模型来代替之前的文件 load 到内存的方式,在性能影响很小的情况下大大减少了内存的使用,值得一试。