应用背景
诸多业务场景下,都有使用kv型式存储数据供快速查询的需求。正常的做法有使用HashMap存入内存,或者存入外部的nosql KV数据库/缓存。
- 使用HashMap做KV存储,速度快,但是如果数据量达到百万及至千万级时,HashMap必将占用大量的java堆内存,给应用带来极大的内存回收压力。
- 外部kv存储,以堆外(offHeap)存储的方式让我们的应用免于内存回收之忧,但其查询性能往往低于内存map。假设采用外部db的方式作kv存储,就会引入服务之间的通信成本,以基于LR(逻辑回归)实现推荐系统的打分服务为例,每次打分,须执行近求成百上万次kv查询(lr参数的查询),如此的查询量对性能的要求是极高的,如果每一次查询都要查询外部服务,那么网络io势必占用大量的时间。
此外,在工作中会发现很多算法问题,都会被转换为一种追求效率的搜索问题,高效的内嵌式kv存储就会显得更有价值。
需求与方案选择
综上所述,本人想要的KV存储,应当满足如下五点要求:
- 内嵌的形式,做为内部存储。这样做的好处在于,能够大大减少通讯成本与性能消耗。而且很多时候,我们所需的kv存储并未达到需要做到分布式的数据量级。
- 采用堆外的方式存储数据。
- 接近于内存Map的查询速度。
- 最好具有硬盘执久化的能力,避免每次重启全量构建。
- 适用于java开发环境。由于我们的各种引擎构建以java为主要开发语言,所以我需要的是一种适用于java的内嵌式kv存储,最好这种存储就是java实现的。
鉴于上述要求,本文选择lucene的mmap存储方式。如果不考虑第五点需求,鼎鼎大名的leveldb是一个很好的选择。leveldb与lucene都可以视作为lsm-tree实现的存储library。原生的leveldb 是用c++实现的。如果以java方式调用,必然会涉及一定量的改造工作。leveldb也有存java实现的版本,据说性能并不逊于lucene,未敢用过。
基于上述要求,我采用lucene4.5.1做kv存储(本文是对之前工作的整体,请不要吐槽我的版本太老),以mmap方式加载lucene索引。由于lucene本身是为搜索设计的,其索引结构远比正常的map形式kv索引复杂得多,存在很多的简化与性能优化的空间。下文描述就是基于lucene MMap实现KV索引,并作不同尝试过程。
以下所有测试采用长度为20的string作为key,int作为value,索引构建条目数为200w。
测试代码地址:https://github.com/quentinxxz/LuceneKVTest
以HashMap为性能参考
参考代码:HashMapTest
首先,以HashMap存储的方式。
每百万次查询,结果如下:
百万次查询平均用时,仅用0.14s,可见HashMap查询的速度相当惊人。
方法1:store存储value,默认压缩
参考代码:LuceneSotreTest.java
value值以lucene store的方式存储。Value field的定义如下: