leveldb性能跟redis key长度度有关吗?

版权声明:本文为博主原创文章未经博主允许不得转载。 /u/article/details/

跳表(skiplist)是一个查询/插入/删除 复杂度o(lgn)的数据结构在查询上跟平衡树的复杂度一致,因此是替代平衡树的方案在redis的zset,leveldb都有应用发现这个算法也解决了我一个问题。各种平衡树在工业界是广泛应用帮助我们快速查找,插入数据但其思想的复雜也让大家不是那么容易接触。当时想直接对有序链表进行二分搜索那岂不是很容易做到相同复杂度且容易理解,不过链表并不能像数組随机访问只能从头一个个遍历。跳表为节点设置了快速访问的指针不同在一个个遍历,可以跨越节点进行访问这也是跳表的名字嘚含义。

上面就是跳表的引子下图就是它的数据结构

当插入一个数据时,随机获得这个节点的高度没错,就是随机!每涨一层的概率為p这个认为设置,一般为0.25或者0.5,这样层数越高的节点就越少(这种结构跟平衡树有点像)

如上图所示,我们检索19这个值遍历路径如下圖所示

可以看到高层级的节点相当于一个快速通道,让搜索进行了节点的跳跃而不是一个个的遍历。

基于java我实现了一个具有增删查的跳表:

搜索实现按照上面的思路从顶层往下遍历,每层之间就是普通链表遍历

插入的思路是要找到插入的点,并且在遍历的同时记录下需要更新层数 快速通道指针的地方,在最后进行处理

例如插入17,并且在17节点随机获得层数是2.这样节点9的第2层(从下往上数)需要指向新嘚节点1712的第1层也要指向17。可以看出需要更新的就是每一层最后遍历过的节点

对get和set方法了解后,删除方法的实现已经没有理解的压力了同理需要记录下搜索的过程中每一层最后遍历的节点。在找到删除节点后把每一层中指向删除节点的 指针指向被删除节点每层的后续指针。

把最底层的链表叫做level0最大层数包括最底层

在数据结构小节中已经说到最大层级为

文中一开始也提到一种情况,16个节点level1可能是9个,level2 3个level3 3个,level14 1个如果我们从第14层开始搜索,有很多无效的工作因为顶层太过稀疏。文中提到最顶层应该是1/p个节点

很容易得到每一层的節点数

文中也对查找的复杂度进行了证明,我截取了部分图可以去原文查看

这里利用递推的思想来进行了证明。

文中基于搜索路径回溯嘚方式进行了证明往上和左进行遍历。假设当前位置在第i层要访问的值为x,i到x之间的层数是k层用c(k)表式遍历k层访问到x的步数。在回溯嘚任何一个节点的移动中都入下图所示。当我们处于情况a的时候我们下一步一定是情况b或者c。
情况c的概率为什么是p呢因为基于当前層,下一层有该节点的概率为p那么情况b自然就是1-p。

便可归纳出 搜索k层需要的步数为: c(k)=k/p

我们一次遍历最大层数上面已经推导出带入步数公式得到一次搜索的步数


因为p是概率常数,一般为1/4或者1/2这样复杂度就是O(lgn)级

文中数据结构的截图也是来自论文。

版权声明:本文为博主原创文章未经博主允许不得转载。 /u/article/details/

一、典型的3种存储引擎

本质为: 基于(内存中)的hash;

所以支持 随机 的增删查改读写的时间复杂度O(1);

但是无法支持順序读写(注,这里指典型的hash不是指如redis的基于跳表的zset的其他功能);

基本效果:在不需要有序遍历时,最优

本质为:基于(磁盘的)顺序查找树B树/B+树;

基本效果:支持有序遍历;但数据量很大后,随机读写效率低(原因往下看);

本质为: 实际落地存储的数据按key划分形成有序的不哃的文件;

结合其“先内存更新后合并落盘”的机制,尽量达到磁盘的写是顺序写尽可能减少随机写;

对于读,需合并磁盘已有历史数據和当前未落盘的驻于内存的更新较慢;

基本效果:也可以支持有序增删查改;写速度大幅提高;读速度稍慢;

1、将随机的写(的操作)保存于内存中,达到一定量后写磁盘;而不是每次都去落盘;

2、落盘的文件(即level0以下层的文件,不含memtable即level0的文件)不论文件内部还是文件相互の间,均按key有序可以二分查找,故无论是读还是其合并操作,均为对数时间;

3、结合1和2尽可能的减少了随机写;实现了有效的按顺序读写;

4、落盘的数据,会定期进行文件合并即由多个小文件的二分查找合并为一个大文件,进一步利于增删查改效率;

lsmtree引擎的读需偠在落盘文件中,查找要查的所有key并查看当前未落盘的更新中是否存在相关的修改,较麻烦但也有优化;

比如二叉查找树、平衡二叉树、红黑树每一个节点,可以存储数据可以存储数据所在地址,可以log(N)的找到增删查改可以有序遍历,到这就是索引;

比如一个有序数組每个数组元素,包括key和valuevalue来存储数据,或者存储数据地址可以按key二分查找;这也是索引;

实际上没有使用1中的二叉树、有序数组来存储数据的;因为:

1、数据量很大时,二叉树非常高需要很多步;

2、有序数组如果随机的插入删除,效果会如何;

B族树最常用于数据库洳mysql的存储引擎:

1、每个节点不再只有一个key而是多个key;相比二叉树,有效压低了树高

2、和典型二叉树一样依然是每个节点包括key和value;这加劇了随机IO问题,也是B+树被使用的原因;

典型B树形似"多叉树",且每个节点包括多个有序节点其访问方法,和二叉树道理一样只是每个樹的节点,包括了多个key而不是二叉树那样只有一个key这样树的高度大大被压低,以其查询伪代码为例加深印象:

如同AVL、红黑树一样增删節点会导致B树分裂;

其读写的算法访问效率是对数级的,了解到这够用

实际的数据库磁盘读写,如果是B树的话会是这样做:

1、最开始創建B树时:

磁盘中申请空间,载入到内存写入;

2、key按顺序不断写入磁盘时:

顺序写入磁盘,速度非常快如按顺序insert大量数据到mysql时,因为:

1、每次都是申请一个磁盘页(如4K大小)而不是要写几个字节申请几个字节;

2、B树顺序写的时候,数据都是向后自然顺延不发生分裂,除非当前磁盘页写满时再申请一个新的磁盘页,继续顺序写;

3、当B树很大时随机的写时:

比如,删除某一个key1然后又更新一个key2的数据,key1囷key2不在同一个磁盘页中然后又增加一个key3导致发生分裂;

比如有大量的这样的操作,导致:

1、寻找key1时内存未找到,被迫从磁盘里读取一個页;

2、删除后可能导致B树分裂可能又导致更新磁盘里的其他数据;

3、增加一个key3,又没有找到再次从磁盘读取一个页;可能导致分裂,再次导致更新磁盘其他页数据

4、更新一个key2又没有找到;

5、最后会发现,数据越来越多后增删查改操作,到处都是磁盘IO经常需要从磁盘里读,然后写;

1、非叶子节点不再存储数据;好处是使每一个磁盘页里,有更多一些的节点;能够减少一些磁盘IO;

2、mysql实际使用时葉子节点加入了相邻叶子节点的指针;好处是,在有序遍历时找到了一个叶子,就可以顺序的访问其他叶子避免了都从根节点遍历;叒减少了一些磁盘IO;

B+树,在非叶子层不再存储数据因而每个树节点变小,也就集中了更多的节点;增多了一个磁盘页中实际节点的个數;这对于减少磁盘IO是有很大意义的;mysql就是使用B+树作为数据索引;

至此,对基于B族树的存储引擎的mysql其查询效率就是:

1、尽可能使按可以嘚顺序写入;

2、在随机不按key顺序的增删查改时,就没办法了B+树尽可能一个磁盘页里有更多的节点,减少磁盘IO次数叶子节点中加入相邻嘚指针,进一步减少磁盘IO更方便范围检索;

现在看一下,同样基于B+树的数据结构mysql两种典型的索引方式的实现:

myisam对于表的主键的索引方式,如一个表有3个列col1、col2、col3

myisam的叶子存储的数据是,数据所在的地址而不是数据内容

下图是myisam的非主键的索引,和主键索引方式差不多也昰存数据的地址:


myisam的索引方式的表,在查询时先通过B+树找到key进而找到数据地址,然后再根据地址找到数据内容;

再来看一下innodb的,使用過mysql的都知道如下准则或建议:

1、innodb的表要求必须有主键;

2、主键尽可能建议是按顺序自增的id;

来看下为什么,下面是innodb的表的主键的索引:

innodb索引方式是把数据完全放在叶子节点上而不是myisam那样只存数据地址;

再来看innodb的表的辅助键的索引:

看吧,innodb的表的辅助键的索引存的其实昰它对应的主键;

1、innodb的索引,其实都是到它的主键索引比如通过辅助键的查询,就是先通过辅助键索引查到对应的主键,然后再去查主键的索引;

这就是为什么innodb的表必须有主键;

同时也是为什么,innodb的表的行锁其实也只是支持字段是主键时的操作时;如果是非主键字段,和myisam是表锁;

innodb的表支持行锁这也是其支持事务的必要条件之一

2、可见innodb的表是非常依赖主键的了,所以如果主键不是按照自增的顺序那么在插入时,会出现B+树更多的分裂内存找不到的又得找磁盘,导致更多的磁盘IO;

这就是为什么innodb的表建议按业务无关的自增id作为主键;

3、如果主键是一个特别长的字符串之类东西,那么辅助键的索引叶子节点也都会存这些特别长的主键,那么辅助键的索引会很大;

這就是为什么,innodb的表不建议主键使用特别长的字段;

4、innodb的叶子存的是数据比起myisam存的是地址,在写时原则上应该会少一些磁盘IO,因为myisam还需要再去获取数据;

上面介绍了基于B族树存储引擎的(mysql)的读写原理结论是:不论是B树,还是B+树在数据量很大时,随机IO问题均无法良好处悝;

大量的随机写导致B族树在数据很大时,出现大量磁盘IO导致速度越来越慢,lsmtree是怎么解决这个问题的:

1、尽可能减少写磁盘次数;

2、即便写磁盘也是尽可能顺序写;

1、对数据,按key划分为若干level;

每一个level对应若干文件包括存在于内存中和落盘了的;

文件内key都是有序的,哃级的各个文件之间一般也有序

2、写时,先写对应于内存的最低level的文件;这是之所以写的快的一个重要原因

存在于内存的数据也是被歭久化的以防丢失;

存在于内存的数据,到达一定大小后被合并到下一级文件落盘;

3、落盘后的各级文件,也会定期进行排序加合并(compact)匼并后数据进入下一层level;

这样的写入操作,大多数的写都是对一个磁盘页顺序的写,或者申请新磁盘页写而不再是随机写

所以总结lsmtree的寫为什么快的两大原因:

1、每次写,都是在写内存;

2、定期合并写入磁盘产生的写都是按key顺序写,而不是随机查找key再写;

首先看一下rocksdb的各级文件组织形式:

然后各级的每个文件,内部都是按key有序除了内存对应的level0文件,各级的内部文件之间也是按key有序的;

这样,查找┅个key很方便二分查找(当然还有bloomfilter等的进一步优化)

再然后,每一级的数据到达一定阈值时会触发排序归并,简单说就是在两个level的文件中,把key有重叠的部分合并到高层level的文件里

良好的归并策略的配置,使数据尽可能集中在最高层(90%以上)而不是中间层,这样有利于compact的速度;

叧外对于基于lsmtree的(rocksdb的)读,需要在各级文件中二分查找磁盘IO也不少,此外还需要关注level0里的对于这个key的操作比较明显的优化是,通过bloomfilter略掉肯定不存在该key的文件减少无谓查找;

当前KV从存储介质可以分为两种模式一种是以内存为主持久化为辅,如memcache(无持久化)、等;一种是以持久化为主内存为辅如ssdb(基于leveldb/rocksdb存储引擎)。这两种模式代表了两种鈈同的选择策略和哲学适应不同的业务场景。简单地说以内存为主的模式侧重高性能,信奉“内存是新的硬盘”的哲学;以持久化为主的模式则侧重大容量兼顾性能。

对于以持久化为主的模式因其天然支持大容量数据的快速读写,实现冷热数据分离是相对比较容易嘚当前用到的数据就认为是热数据,只要把热数据保留于指定大小的内存即可而对于以内存为主的模式,即使有持久化也只是顺序寫的持久化,在需要读硬盘时不能做到快速读取因此这种模式要实现冷热分离,需要准确区分冷热数据、精心设计落地策略并保证可以赽速读取

这里冷热分离方案主要基于或者基于redis协议及命令实现。


ü  写操作全部记录在内存不同步写磁盘;

ü  常驻写子进程定时将内存Φ的数据写到磁盘;

ü  内存中标记不存在的key,如果一个key在磁盘上不存在则在标记之后不用再去磁盘查看这个值是否存在;

ü  读操作先读內存,如内存中不存在且key未被标识磁盘不存在则由读子进程从磁盘读并写回到redis(key不存在才写回)。之后子进程通知主进程再次读取此過程会阻塞主进程上单个连接的处理。

l  优点:真正意义上实现单机redis的冷热分离Redis和落地数据在同一台机器,容易保证数据一致性

l  缺点:實现较复杂。因为是基于redis做二次开发后续不方便升级redis,不过单机redis已经非常稳定后续升级可能性较小。

redis定位是内存KV数据库只支持所有數据存放在内存,持久化只是数据安全性的一种保障方式基于redis做冷热分离的例子有两个,一个是v2.0-2.4版的原生redis一个是jimdb S,这两个做得都不成功

redis 2.0版加入支持VM机制,VM机制即虚拟内存机制参考的虚拟内存机制实现,暂时把不经常访问的数据从内存交换到磁盘中需要时再从磁盘茭换回内存,可以实现冷热数据分离但由于VM机制在某些情况下会导致redis重启、保存和同步数据等太慢及代码复杂,所以2.4版后就不再支持了

S可以保存全量数据,把缓存+数据库的两层用一层架构取代写操作时先写内存,再异步写cycledb读操作如数据不在内存,则创建后台任务读cycledb这个后台任务的作用是预热,读到数据后并不把结果载入内存执行完成后由前台主线程再次读取,这次在内存中读不到则直接读取cycledb并載入内存目前了解到的情况是使用不广泛,而且即将下线主要原因是性能不如纯内存的redis,但不知道是否还有其它缺陷

S的实践情况看,直接改造redis做冷热分离可能并不是一个很好的发展方向即使做出来也很可能是一个平庸的产品。最根本的原因是纯内存的redis性能更好而鼡户对性能的期望是没有最好,只有更好随着内存越来越大、越来越便宜,更多的数据可以直接放到内存会进一步导致冷热分离成为┅个鸡肋功能。另外一个原因是实现冷热分离会导致redis代码复杂性增加不少不利于后续的维护。


l  优点:写操作和当前流程完全一样;读操莋和当前迁移流程中rrw流程基本一致可以复用。不影响纯内存的原生redis使用风险可控。

l  缺点:proxy和redis均需修改在原有一主一备redis基础上需要增加改造备redis部署。

最大特点是不影响纯内存的原生redis使用且proxy改动较小。

可以视情况选择部署一个或两个改造备redis只部署一个改造备redis时落地数據是单点,可用于数据丢失不重要或后端另存有全量数据的场景部署两个改造备redis可以避免单点,因为是链式同步对主redis几乎无影响。


l  缺點:proxy实现较复杂redis和ssdb的数据一致性不好保证。因为ssdb基于leveldb/rocksdb实现在读操作且redis中无数据且ssdb内存中无数据时,可能极大影响性能


l  实现描述:redis和ssdb獨立两套系统,类似之前腾讯提供CMEM和TSSD两套系统业务开发根据业务特点决定使用哪一套系统。client、proxy可以复用等同可以选择使用redis存储引擎或leveldb/rocksdb存储引擎。

l  优点:无须开发只需引入ssdb系统即可。

l  缺点:业务开发可能没办法一开始确定使用哪一套系统需要维护和运维两套系统。

方案五 提供ssdb业务初始接入redis,可选择平滑迁入ssdb


l  实现描述:类似方案四但可选择从redis平滑迁入ssdb。

l  缺点:需要维护和运维两套系统

不管是直接基于leveldb/rocksdb做数据落地,还是使用ssdb都会碰到读操作且redis中无数据的性能问题,因为此时需要先读取redisredis中读不到再一个level一个level去读取磁盘文件,这种凊况的性能可想而知不会太好

redis区分冷热数据都是设定redis的maxmemory,然后进行lru淘汰使内存中只保留热数据而Redis的lru淘汰只是从随机选的一些key选出最符匼lru规则的一个key进行淘汰,即只是一种近似淘汰所以不能很好地区分冷热数据。因此有可能出现被lru淘汰的key实际并不是冷数据这样下次读取时会因为redis中已无数据而去磁盘读,出现一些性能问题

l  写操作先写内存还是先写磁盘?

先写内存此时如果系统崩溃,内存中的数据还沒有来得及Dump到磁盘会丢失数据。先写磁盘再写内存,则即使系统崩溃不会造成数据的丢失,但可能导致磁盘和内存数据的不一致為了避免这种不一致,又得先删除内存中的key再写磁盘,再写内存影响性能。总的来说对于以内存为主的KV数据库,优先选择先写内存

为了提高读写磁盘的性能,需要使用SSD而SSD本身存在一些问题——

?      坏块问题:SSD盘中可能存在某个块可以写入,但是读不出来此时这个塊的数据将会丢失。

总的来说基于redis做冷热分离从技术上是可行的,从业务实用角度看却不一定因为首先redis不能很好区分冷热数据,然后佷难避免读取落地冷数据时的性能问题因此肯定不如纯内存的redis性能好,而用户对KV数据库性能的期望是没有最好只有更好。随着内存越來越大、越来越便宜更多的数据可以直接放到redis内存,会进一步导致冷热分离成为一个无人使用的鸡肋功能

最理想的冷热分离是redis能够满足如下三个条件:

l  lru淘汰做到淘汰的是最冷数据;

l  能快速从磁盘读取冷数据并写回到内存。

这样就能在maxmemory足够时完全是纯内存KV数据库模式不受冷数据落地的丁点影响;而在maxmemory满时进入冷热数据分离模式,又能在设大maxmemory时恢复纯内存KV数据库模式当然要达到这个目标非常难。

从实际栲虑当前有业务确实想要冷热分离的话,建议方案四即提供两套KV系统——以内存为主的redis系统和以持久化为主的ssdb系统,根据业务特点选擇使用考虑现有已使用redis的业务需要迁到ssdb的话,则优先选择方案五

附各C/C++持久化KV数据库简介和分析:

LevelDb是google开源的能够处理十亿级别规模KV型数據持久性存储的C++程序库。LevleDb在存储数据时是根据记录的key值有序存储。LevelDb的写操作要大大快于读操作而顺序读写操作则大大快于随机读写操莋。

level一个写操作仅涉及一次磁盘文件追加写和内存SkipList插入操作因此leveldb的写操作非常高效。

由于 LevelDB 在某一层查找不存在的数据时, 会继续在下一层進行查找, 所以对于不存在的数据的查找会速度非常慢. 所以, 需要结合 Bloom Filter, 利用 Bloom Filter 能快速地判定”不存在”的特点

Facebook 维护的一个活跃的 LevelDB 的分支。RocksDB 在 LevelDB 上莋了很多的改进比如多线程 Compactor、分层自定义压缩、多 MemTable 等。另外 RocksDB 对外暴露了很多配置项可以根据不同业务的形态进行调优。

LMDB 选择在内存映潒文件 (mmap) 实现 B+Tree而且同时使用了 Copy-On-Write 实现了 MVCC 实现并发事务无锁读的能力,对于高并发读的场景比较友好;同时因为使用的是 mmap 所以拥有跨进程读取嘚能力

基于leveldb/rocksdb存储引擎实现,加入网络支持兼容redis协议和redis数据类型(不支持set集合),支持主从复制和负载均衡SSDB/LevelDB 在进行数据库整理(Compaction)操作时, 磁盘io高,持续的时间一般随着数据变大而变长, 一般只持续数秒不能指定执行compaction的时间。有些redis命令不支持有些支持的redis命令可能不完全和redis一致。

分布式文档数据库的最大卖点是不需构建非主键索引也能执行很多查询。

SSD上实现的memcached内存中保存索引数据,机器重启索引数据会丢夨假如只需要支持string类型数据落地,可使用代替ssdb

淘宝开源的分布式KV数据库。抽象存储层的架构设计使Tair很容易接入新的存储引擎当前支歭的存储引擎有非持久化的MDB(自主研发的类memcache)/RDB(抽离Redis的存储部分),持久化的LDB(接入LevelDB)

非严格意义上的KV数据库。支持和两种工作模式无數据源模式就是一个简单的基于共享内存的cache服务。持久数据源后接写操作先写db,再写内存;读操作先读内存内存中不存在再去读db,读db荿功再添加到内存所以TTC本质上是一个带缓存功能的数据库,可以想见其读写性能肯定不如纯内存数据库也没有leveldb高效写的特点,注定是┅个平庸的产品再加上使用繁琐,早已被淘汰

分布式KV内存数据库,支持主从同步和平滑迁移数据可落地,高性能仅做KV缓存还不错,但支持数据类型和命令不如redis丰富

我要回帖

更多关于 key的实际长度 的文章

 

随机推荐