赞
踩
本文为之前阅读leveldb源码时,结合网上一些博客见解以及自己的理解组合成的一篇文章,本文会从总体以及主要的数据结构去阐述什么是leveldb。文末有部分参考的文章的链接,如不够全面,希望能联系我补全。
写多读少。
LSM-Tree(Log Structured-Merge Tree),简单的说,其牺牲了部分读取的性能,通过批量顺序写来换取了高吞吐的写性能,这种特性在大数据领域得到充分了体现。换句话说,核心思想的核心就是放弃部分读能力,换取写入的最大化能力。除了leveldb,还有其他的NoSQL数据底层数据结构也使用了LSM-Tree,如Hbase,Cassandra,RocksDB(FaceBook基于leveldb的优化版本)。
LSM-Tree里面,核心的数据结构就是SSTable(Sorted String Table),当一个SSTable被打开的时候,index会被加载到内存,然后根据key在内存index里面进行一个二分查找,查到该key对应的磁盘的offset之后,然后去磁盘把响应的块数据读取出来。如果内存足够大的话,可以直接把SSTable直接通过MMap的技术映射到内存中,从而提供更快的查找。
SSTable分为两部分,一部分在内存中Mentable与Immutable(leveldb使用了跳表来实现),剩下的储存在多级磁盘里面,level0~levelN文件,这也是leveldb名字的由来。具体结构图如下,以一个写的操作为例
总的来说,一次写的操作,先后操作为一次顺序写的日志操作,以及一次时间复杂度为O(log n)的内存操作(跳表)。
简单谈一下读的操作
当收到一个读请求的时候,会直接先在内存里面查询,如果查询到就返回。
如果没有查询到就会依次下沉,直到把所有的Level层查询一遍得到最终结果。
查看源码可发现,在调用open打开一个数据库时,首先回去检查是否有需要恢复的内容,可恢复的,主要为以下四种可能
写log期间进程异常;
数据库重启时,发现异常日志数据的话,为了保障一致性,会抛弃该条日志数据,也就是认为用户写入失败
写log完成,写内存未完成;
write动作完成(即log、内存写入都完成)后,进程异常;
Immutable memtable持久化过程中进程异常;
2~4类情况发生的话,都可以通过日志文件中的记录完成数据库的恢复
注:
事实上,使用WALLog并没有完全消除了写入丢失的问题。写完日志文件之后,操作系统并不是马上将这些数据真正落到磁盘中,而是暂时保存在缓存中,而ubuntu中常用sync来强行将数据落到磁盘中。若用户完成写入操作,操作系统还没有将数据落盘时,系统崩了,就会造成写丢失。如果只是进程异常退出,就不存在这个问题,重启即可以恢复
从查找的资料以及个人的想法来看:
1.跳表其实是利用概率均衡技术,可以实现高效的查询、插入、删除操作,复杂度在大多数情况下为O(logN)。
2.跳表基于链表,在实现上会更简单,简单的结构在后期的维护上更占优势
从以上两点看,作者目的为借助概率平衡来构建一个快速且简单的数据结构来取代平衡树。因为平衡树属于一种非常复杂的数据结构,为了维持树的平衡,获得稳定的查询效率,平衡每次插入都可能涉及到较为复杂的节点旋转等操作。
更直白地说,跳表简单且效率并不比平衡树差很多,取代平衡树是一个极优的选择。
主要是提升写的性能,单条写性能肯定没有批量写好,同时对于查找新增的数据可以直接查询,能够避免一定的IO
SSTable 是可以启用压缩功能的,并且这种压缩不是将整个 SSTable 一起压缩,而是根据 locality 将数据分组,每个组分别压缩,这样的好处当读取数据的时候,我们不需要解压缩整个文件而是解压缩部分 Group 就可以读取。
因为SSTable在写入磁盘后,除了Compaction之外,是不会变化的,所以我可以将Scan的Block进行缓存,从而提高检索的效率
正常情况下,一个读操作是需要读取所有的 SSTable 将结果合并后返回的,但是对于某些 key 而言,有些 SSTable 是根本不包含对应数据的,因此,我们可以对每一个 SSTable 添加 Bloom Filter,因为布隆过滤器在判断一个SSTable不存在某个key的时候,那么就一定不会存在,利用这个特性可以减少不必要的磁盘扫描。
这个在前面的写入流程中已经介绍过,通过定期合并瘦身, 可以有效的清除无效数据,缩短读取路径,提高磁盘利用空间。但Compaction操作是非常消耗CPU和磁盘IO的,尤其是在业务高峰期,如果发生了Major Compaction,则会降低整个系统的吞吐量,这也是一些NoSQL数据库,比如Hbase里面常常会禁用Major Compaction,并在凌晨业务低峰期进行合并的原因。
B+树会更好一些。
在数据的更新和删除方面,B+Tree可以做到原地更新和删除,这种方式对数据库事务支持更加友好,因为一个key只会出现一个Page页里面,但由于LSM-Tree只能追加写,并且在L0层key的rang会重叠,所以对事务支持较弱,只能在Segment Compaction的时候进行真正地更新和删除。
优点:主要是支持高效的读(稳定的O(logN))
缺点:在大规模的写请求下(复杂度O(LogN)),效率会变得比较低,因为随着insert的操作,为了维护B+树结构,节点会不断的分裂和合并。操作磁盘的随机读写概率会变大,故导致性能降低。
简单谈一下B+树的特性
一般关系型数据库的底层数据结构都选用了它,如MySQL。
关于第一点,如关键字2 5 8对应三颗子树。
特点总的来说是,将所有数据都存在叶子节点中,所有非叶子节点均为索引,如果进行IO操作,会比B树的IO操作更少。同时,所有叶子节点都是连起来的,具备了链表的特性,同时使用在范围查询时更加方便。
优点:支持高吞吐的写(可认为O(1)),经过优化后(如索引、缓存等),读的效率大部分情况下能达到O(logN)的复杂度(在读方面B+树更优)
缺点:高吞吐的写会使得系统频繁进行compaction(compare & merge),写入量越大,compaction越频繁,会非常消耗CPU和储存IO,占用大量系统资源,带来整个系统性能的下跌。因此,必要情况下需要禁用自动compaction,可以在低峰期定期触发合并。
leveldb拥有了数据库基本的增删查改操作,以下为一个基本的例子
#include <cassert> #include <iostream> #include <string> #include <leveldb/db.h> int main() { leveldb::DB* db; leveldb::Options options; options.create_if_missing = true; leveldb::Status status = leveldb::DB::Open(options, "./tmp/testdb", &db); assert(status.ok()); std::string key = "apple"; std::string value = "B"; std::string get; // 写入 leveldb::Status s = db->Put(leveldb::WriteOptions(), key, value); // 取出 if (s.ok()) s = db->Get(leveldb::ReadOptions(), key, &get); if (s.ok()) std::cout << "读取到的与(key=" << key << ")对应的(value=" << get << ")" << std::endl; else std::cout << "读取失败!" << std::endl; delete db; return 0; }
参考链接:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。