LevelDB是google公司开发出来的一款 超高性能kv存储引擎,以其惊人的读性能和更加惊人的写性能在轻量级nosql数据库中鹤立鸡群. 此开源项目目前是支持处理十亿级别规模Key-Value型数据持久性存储的C++ 程序库。在优秀的表现下对于内存的占用也非常小,他的大量数据都直接存储在磁盘上.可以理解为以空间换取时间.
任何东西都不是十全十美的,LevelDB也有它的局限性:
LevelDB 只是一个 C/C++ 编程语言的库, 不包含网络服务封装, 所以无法像一般意义的存储服务器(如 MySQL)那样, 用客户端来连接它, 使用者应该封装自己的网络服务器.
node.js下如何使用LevelDB ?
node.js环境下使用需要npm 包 levelUP,levelDown 来支持.
npm install levelUP levelDown
或者你也可以这样npm install level
提醒:levelup 版本最好用0.10.0或者更低版本,最新版本和leveldown编译时出问题. npm 安装指定版本依赖包 所以这样比较安全npm install [email protected] leveldown
如何使用?
api定义非常简单.
var levelup = require(‘levelup‘);
var db = levelup(‘./yijiebuyi‘); //这里的路径就是物理存储数据的文件路径,建议不要放到项目中.
下面是常用的获取,设置api (引用自 github node-levelup https://github.com/rvagg/node-levelup)
// put a key & value
db.put(‘name‘, ‘LevelUP‘, function (err) {
if (err) return console.log(‘Ooops!‘, err) // some kind of I/O error
// fetch by key
db.get(‘name‘, function (err, value) {
if (err) return console.log(‘Ooops!‘, err) // likely the key was not found
// ta da!
console.log(‘name=‘ + value)
})
})
一介布衣博客 采用了node.js + leveldb 方式,上面的这个数据库封装类也是一介布衣博客使用的一个通用帮助文件.
关于levelDB的api我简单做了一个封装,代码如下:
//设置
function put(key, value, callback) {
if (key && value) {
db.put(key, value, function (error) {
callback(error);
})
} else {
callback(‘no key or value‘);
}
}
//获取
function get(key, callback) {
if (key) {
db.get(key, function (error, value) {
callback(error, value);
})
} else {
callback(‘no key‘, key);
}
}
//删除
function del(key, callback) {
if (key) {
db.del(key, function (error) {
callback(error);
})
} else {
callback(‘no key‘);
}
}
//批量操作
function batch(arr, callback) {
if (Array.isArray(arr)) {
var batchList = [];
arr.forEach(item)
{
var listMember = {};
if (item.hasOwnProperty(‘type‘)) {
listMember.type = item.type;
}
if (item.hasOwnProperty(‘key‘)) {
listMember.key = item.key;
}
if (item.hasOwnProperty(‘value‘)) {
listMember.value = item.value;
}
if (listMember.hasOwnProperty(‘type‘) && listMember.hasOwnProperty(‘key‘) && listMember.hasOwnProperty(‘value‘)) {
batchList.push(listMember);
}
}
if (batchList && batchList.length > 0) {
db.batch(batchList, function (error) {
callback(error, batchList);
})
} else {
callback(‘array Membre format error‘);
}
} else {
callback(‘not array‘);
}
}
//查找 (支持前置匹配)
function find(find, callback) {
var option = {keys: true, values: true, revers: false, limit: 20, fillCache: true};
if (!find)
return callback(‘nothing‘, null);
else {
if (find.prefix) {
option.start = find.prefix;
option.end = find.prefix.substring(0, find.prefix.length - 1)
+ String.fromCharCode(find.prefix[find.prefix.length - 1].charCodeAt()+1);}if(find.limit)
option.limit = find.limit;
db.createReadStream(option).on(‘data‘,function(data){
data&&callback(data.key, data.value);}).on(‘error‘,function(err){}).on(‘close‘,function(){}).on(‘end‘,function(){return callback(null,Date.now());});}}
exports.put = put;
exports.get=get;
exports.del=del;
exports.find = find;
exports.batch = batch;
代码注释的不详细,我直接拷贝过来的,方法名基本保持和api一致,大家应该能看明白 里面的 get ,put ,delete 都非常好理解,就是根据key去查询value ,插入一对key和value ,根据key删除value. find 方法可能不是特别好理解,在levelDB存储复杂数据结构中讲到用前置匹配的方法来查询索引正是用到了createReadStream 方法,这是原生api里的方法,我利用这个api封装成了 find() 方法.下面主要说一下前置匹配.
createReadStream 方法的前置匹配
回顾一下,我库里有如下key-value 键值对 abc --> 111 abd --> 333 abm --> 777 abw --> 999 那么我可以用createReadStream方法查询出以 ab开头的key 对应的所有value,安装key的读取顺序把value一个一个返回.我们贴出createReadStream调用代码:
db.createReadStream()
.on(‘data‘, function (data) { console.log(data.key, ‘=‘, data.value)
})
.on(‘error‘, function (err) { console.log(‘Oh my!‘, err)
})
.on(‘close‘, function () { console.log(‘Stream closed‘)
})
.on(‘end‘, function () { console.log(‘Stream closed‘)
})
它监听了一个读取流,要触发如下事件 on(‘data’ 将读取到的value按照读取顺序返回. on(‘error’ 发生错误时要做什么 on(‘close’ 关闭流时要做什么 on(‘end’ 流结束时做什么 其实这个api可以接受一个参数,参数可以指定key的开始位置 ,结束位置,然后就可以做到前置匹配,我把此方法封装了一下,在levelup使用方法一文中已经贴出了代码,下面再单独把此方法的封装贴一下
function find(putoption, callback) {
var option = {keys: true, values: true, revers: false, limit: 20, fillCache: true};
if (!putoption)
return callback(‘nothing‘, null);
else {
if (putoption.prefix) {
option.start = find.prefix;
option.end = find.prefix.substring(0, find.prefix.length - 1)
+ String.fromCharCode(find.prefix[find.prefix.length - 1].charCodeAt() + 1);
}
if (putoption.limit)
option.limit = find.limit;
db.createReadStream(option).on(‘data‘,function (data) {
data&&callback(data.key, data.value);
}).on(‘error‘,function (err) {
}).on(‘close‘,function () {
}).on(‘end‘, function () {
return callback(null, Date.now());
});
}
}
putoption 是外部出入的参数对象, option 是内部组合的参数对象, createReadStream 可以指定读取key的开始位置(如 option.start ),结束位置(option.end ),以及读取多少条记录 (option.limit)
使用场景,就拿一介布衣博客博文存储的数据结构来说起:
一介布衣 博客中有nosql分类,点击nosql应该加载此分类下的所有博文并分页. 首先,博文的存储类型如下 key: blog.fse89fw8fwe89fwe98fweiwe9f value: {_id:’fse89fw8fwe89fwe98fweiwe9f’,title:’levelDB存储复杂数据结构’,content:’省略500字’,category:’nosql’,tag:’nosql,levelDB,levelDB存储结构’,create_time:13987546090,click:10}
如上,key为了全部全局唯一 所以用常量 blog.+博文唯一ID value 是一个字符串,我们读出来后需要反序列化成json来使用,包括唯一ID,标题,内容,类别,标签,创建时间和点击数
到这里并没有结束,你还需要做很多工作,比如我根据类别查询博客,根据标签查询博客,根据时间统计,排序等… 所有的kv数据库貌似都是根据key查询value ,而不能逆向查询 (如果有,请告诉我) levelDB也是,所以我们下面要创建一系列的索引来支撑上面的查询工作.
索引的key本人规定 .开头,只是为了维护,你可以命名自己的索引是 芝麻开门 开头或者更奇葩的都行. 类别索引 key : .category.nosql:fse89fw8fwe89fwe98fweiwe9f value : fse89fw8fwe89fwe98fweiwe9f
这个索引的作用就是,通过 nosql 我要找到属于这个类别的博文ID,然后根据ID找到博文 说明一下,key组成结构 常量 .category + 类别名 .nosql + 博文ID fse89fw8fwe89fwe98fweiwe9f value 组成结构 很简单,就是一个博文ID
你也许有疑问:
1.key里面为啥要有 前置常量 .category ,因为我还有标签的索引 .tag 开头的,只是为了区分 2.既然 category 后面已经加上 nosql 类别,为啥还要加一个 博文ID,因为保证此索引是唯一的,我明天继续写一篇关于 levelDB 的文章,他的类别还是 nosql,如果没有这个博文ID,那么就把今天的key完全覆盖了.我的索引是没有意义的. 3.你的索引就是为了查找 博文ID,我需要通过 key 来查找 value,这不是骑着毛驴找毛驴吗? 开始这个问题确实让人迷糊,首先你要同意上面第二点的说明,接着我们来说明如何在没有博文ID的情况下使用此索引找到博文ID 因为levelDB有一项强大的功能,前置匹配,他可以用key的子集去匹配类似的key,注意这里有个前提,必须的从key的0位置开始,就是从头匹配 比如 key是 abcdef 你可以用 a 匹配到这个key,用ad匹配到,用 abc,abcd,abcde 都可以.但是不能用 bcdef来匹配,这样是不行的.
再回头看第三个问题,我根据类别找博文的时候是用下面的key .category.nosql: 用这个key 我可以匹配到 N个key ,这些key对应的博文ID 都是nosql相关的,然后我再遍历博文ID获取博文