高性能nosql ledisdb设计与实现(1)
ledisdb是一个用go实现的基于leveldb的高性能nosql数据库,它提供多种数据结构的支持,网络交互协议参考redis,你可以很方便的将其作为redis的替代品,用来存储大于内存容量的数据(当然你的硬盘得足够大!)。
同时ledisdb也提供了丰富的api,你可以在你的go项目中方便嵌入,作为你app的主要数据存储方案。
与redis的区别
ledisdb提供了类似redis的几种数据结构,包括kv,hash,list以及zset,(set因为我们用的太少现在不予支持,后续可以考虑加入),但是因为其基于leveldb,考虑到操作硬盘的时间消耗铁定大于内存,所以在一些接口上面会跟redis不同。
最大的不同在于ledisdb对于在redis里面可以操作不同数据类型的命令,譬如(del,expire),是只支持kv操作的。也就是说,对于del命令,ledisdb只支持删除kv,如果你需要删除一个hash,你得使用ledisdb额外提供的hclear命令。
为什么要这么设计,主要是性能考量。leveldb是一个高效的kv数据库,只支持kv操作,所以为了模拟redis中高级的数据结构,我们需要在存储kv数据的时候在key前面加入相关数据结构flag。
譬如对于kv结构的key来说,我们按照如下方式生成leveldb的key:
func (db *DB) encodeKVKey(key []byte) []byte {
ek := make([]byte, len(key)+2)
ek[0] = db.index
ek[1] = kvType
copy(ek[2:], key)
return ek
}
kvType就是kv的flag,至于第一个字节的index,后面我们在讨论。
如果我们需要支持del删除任意类型,可能的一个做法就是在另一个地方存储该key对应的实际类型,然后del的时候根据查出来的类型再去做相应处理。这不光损失了效率,也提高了复杂度。
另外,在使用ledisdb的时候还需要明确知道,它只是提供了一些类似redis接口,并不是redis,如果想用redis的全部功能,这个就有点无能为力了。
db select
redis支持select的操作,你可以根据你的业务选择不同的db进行数据的存放。本来ledisdb只打算支持一个db,但是经过再三考虑,我们决定也实现select的功能。
因为在实际场景中,我们不可能使用太多的db,所以select db的index默认范围就是[0-15],也就是我们最多只支持16个db。redis默认也是16个,但是你可以配置更多。不过我们觉得16个完全够用了,到现在为止,我们的业务也仅仅使用了3个db。
要实现多个db,我们开始定了两种方案:
- 一个db使用一个leveldb,也就是最多ledisdb将打开16个leveldb实例。
- 只使用一个leveldb,每个key的第一个字节用来标示该db的索引。
这两种方案我们也不知道如何取舍,最后决定采用使用同一个leveldb的方式。可能我们觉得一个leveldb可以更好的进行优化处理吧。
所以我们任何leveldb key的生成第一个字节都是存放的该db的index信息。
KV
kv是最常用的数据结构,因为leveldb本来就是一个kv数据库,所以对于kv类型我们可以很简单的处理。额外的工作就是生成leveldb对应的key,也就是前面提到的encodeKVKey的实现。
Hash
hash可以算是一种两级kv,首先通过key找到一个hash对象,然后再通过field找到或者设置相应的值。
在ledisdb里面,我们需要将key跟field关联成一个key,用来存放或者获取对应的值,也就是key:field这种格式。
这样我们就将两级的kv获取转换成了一次kv操作。
另外,对于hash来说,(后面的list以及zset也一样),我们需要快速的知道它的size,所以我们需要在leveldb里面用另一个key来实时的记录该hash的size。
hash还必须提供keys,values等遍历操作,因为leveldb里面的key默认是按照内存字节升序进行排列的,所以我们只需要找到该hash在leveldb里面的最小key以及最大key,就可以轻松的遍历出来。
在前面我们看到,我们采用的是key:field的方式来存入leveldb的,那么对于该hash来说,它的最小key就是"key:",而最大key则是"key;",所以该hash的field一定在"(key:, key;)"这个区间范围。至于为什么是“;”,因为它比":"大1。所以"key:field"一定小于"key;"。后续zset的遍历也采用的是该种方式,就不在说明了。
List
list只支持从两端push,pop数据,而不支持中间的insert,这样主要是为了简单。我们使用key:sequence的方式来存放list实际的值。
sequence是一个int整形,相关常量定义如下:
listMinSeq int32 = 1000
listMaxSeq int32 = 1<<31 - 1000
listInitialSeq int32 = listMinSeq + (listMaxSeq-listMinSeq)/2
也就是说,一个list最多存放1<<31 - 2000条数据,至于为啥是1000,我说随便定得你信不?
对于一个list来说,我们会记录head seq以及tail seq,用来获取当前list开头和结尾的数据。
当第一次push一个list的时候,我们将head seq以及tail seq都设置为listInitialSeq。
当lpush一个value的时候,我们会获取当前的head seq,然后将其减1,新得到的head seq存放对应的value。而对于rpush,则是tail seq + 1。
当lpop的时候,我们会获取当前的head seq,然后将其加1,同时删除以前head seq对应的值。而对于rpop,则是tail seq - 1。
我们在list里面一个meta key来存放该list对应的head seq,tail seq以及size信息。
ZSet
zset可以算是最为复杂的,我们需要使用三套key来实现。
- 需要用一个key来存储zset的size
- 需要用一个key:member来存储对应的score
- 需要用一个key:score:member来实现按照score的排序
这里重点说一下score,在redis里面,score是一个double类型的,但是我们决定在ledisdb里面只使用int64类型,原因一是double还是有浮点精度问题,在不同机器上面可能会有误差(没准是我想多了),另一个则是我不确定double的8字节memcmp是不是也跟实际比较结果一样(没准也是我想多了),其实更可能的原因在于我们觉得int64就够用了,实际上我们项目也只使用了int的score。
因为score是int64的,我们需要将其转成大端序存储(好吧,我假设大家都是小端序的机器),这样通过memcmp比较才会有正确的结果。同时int64有正负的区别,负数最高位为1,所以如果只是单纯的进行binary比较,那么负数一定比正数大,这个我们通过在构建key的时候负数前面加"<",而正数(包括0)加"="来解决。所以我们score这套key的格式就是这样:
key<score:member //<0
key=score:member //>=0
对于zset的range处理,其实就是确定某一个区间之后通过leveldb iterator进行遍历获取,这里我们需要明确知道的事情是leveldb的iterator正向遍历的速度和逆向遍历的速度完全不在一个数量级上面,正向遍历快太多了,所以最好别去使用zset里面带有rev前缀的函数。
总结
总的来说,用leveldb来实现redis那些高级的数据结构还算是比较简单的,同时根据我们的压力测试,发现性能还能接受,除了zset的rev相关函数,其余的都能够跟redis保持在同一个数量级上面,具体可以参考ledisdb里面的性能测试报告以及运行ledis-benchmark自己测试。
后续ledisdb还会持续进行性能优化,同时提供expire以及replication功能的支持,预计6月份我们就会实现。
ledisdb的代码在这里https://github.com/siddontang/ledisdb,希望感兴趣的童鞋共同参与。
高性能nosql ledisdb设计与实现(1)的更多相关文章
- 高性能nosql ledisdb设计与实现 (2):replication
ledisdb现在已经支持replication机制,为ledisdb的高可用做出了保障. 使用 假设master的ip为10.20.187.100,端口6380,slave的ip为10.20.187 ...
- 发布一个参考ssdb,使用go类似的实现redis高性能nosql:ledisdb
起因 ledisdb是一个參考ssdb.採用go实现,底层基于leveldb,相似redis的高性能nosql数据库,提供了kv,list,hash以及zset数据结构的支持. 我们如今的应用极大的依 ...
- 发布一个参考ssdb,用go实现的类似redis的高性能nosql:ledisdb
起因 ledisdb是一个参考ssdb,采用go实现,底层基于leveldb,类似redis的高性能nosql数据库,提供了kv,list,hash以及zset数据结构的支持. 我们现在的应用极大的依 ...
- 《高性能CUDA应用设计与开发》--笔记
第一章 1.2 CUDA支持C与C++两种编程语言,该书中的实例采取的是Thrust数据并行API,.cu作为CUDA源代码文件,其中编译器为ncvv. 1.3 CUDA提供多种API: 数据并行 ...
- NoSql数据库 设计上面的一些心得
NoSql数据库这个概念听闻许久了,也陆续看到很多公司和产品都在使用,优缺点似乎都被分析的清清楚楚.但我心里一直存有一个疑惑,它的出现究竟是为了解决什么问题? 用户信息表,书籍信息表,用户为书籍打分信 ...
- 选择高性能NoSQL数据库的5个步骤
来源:Redislabs作者:Shabih Syed 翻译:Kevin (公众号:中间件小哥) 构建在线和运营应用程序的开发团队越来越多地选择一类新的数据库来支持它们.它被称为“NoSQL”或“Not ...
- 高性能网站架构设计之缓存篇(5)- Redis 集群(上)
集群技术是构建高性能网站架构的重要手段,试想在网站承受高并发访问压力的同时,还需要从海量数据中查询出满足条件的数据,并快速响应,我们必然想到的是将数据进行切片,把数据根据某种规则放入多个不同的服务器节 ...
- 高性能网站架构设计之缓存篇(2)- Redis C#客户端
在上一篇中我简单的介绍了如何利用redis自带的客户端连接server并执行命令来操作它,但是如何在我们做的项目或产品中操作这个强大的内存数据库呢?首先我们来了解一下redis的原理吧. 官方文档上是 ...
- 高性能网站架构设计之缓存篇(1)- Redis的安装与使用
一.什么 Redis REmote DIctionary Server,简称 Redis,是一个类似于Memcached的Key-Value存储系统.相比Memcached,它支持更丰富的数据结构,包 ...
随机推荐
- Linux学习之CentOS(十五)----磁盘管理之 启动挂载(转)
启动挂载 /etc/fstab 及 /etc/mtab 刚刚上面说了许多,那么可不可以在启动的时候就将我要的文件系统都挂好呢?这样我就不需要每次进入 Linux 系统都还要在挂载一次呀!当然可以啰!那 ...
- Java 反射(二)
作者:郑剑锋链接:https://www.zhihu.com/question/24304289/answer/147529485来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注 ...
- Filter,FilterChain,FilterConfig
实例: package com.zillion.app.filter; import java.io.IOException; import javax.servlet.Filter; import ...
- struts框架从.jsp页面直接访问action
<%@ page language="java" pageEncoding="UTF-8"%><%String path = request. ...
- ubuntu + 不识别无线网卡简易处理方式 + 需有线联网
进入ubuntu, 连接有线网; 系统设置 -> 软件和更新 -> 附加驱动 -> 找到无线网卡驱动并更新 若无候选的无线网卡驱动,我就没招了.
- Linux 管理软件
公司的openfire先前运行在windows上的,但由于在windows上openfire内存机制问题,最多只能占用2GB内存,且时间稍微长久一些就会自动挂掉,用户无法登陆和连接,因此迁移到了Cen ...
- ACM 悼念512汶川大地震遇难同胞——珍惜现在,感恩生活
Problem Description 急!灾区的食物依然短缺!为了挽救灾区同胞的生命,心系灾区同胞的你准备自己采购一些粮食支援灾区,现在假设你一共有资金n元,而市场有m种大米,每种大米都是袋装产品, ...
- MySQL where 子句
MySQL where 子句 我们知道从MySQL表中使用SQL SELECT 语句来读取数据. 如需有条件地从表中选取数据,可将 WHERE 子句添加到 SELECT 语句中. 语法 以下是SQL ...
- ZooKeeper之(四)配置与命令
4.1 配置文件 ZooKeeper安装好之后,在安装目录的conf文件夹下可以找到一个名为"zoo_sample.cfg"的文件,是ZooKeeper配置文件的模板. ZooKe ...
- 多线程(五) Fork/Join框架介绍及实例讲解
什么是Fork/Join框架 Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架. 我们再通过For ...