赞
踩
本来想写点B+树的,不过B+树因为用在Mysql等关系型数据库中,大家都比较了解了,而LSM树这种索引设计思路主要用在NoSql中,如果没有接触过NoSQL数据库的朋友可能了解不多,就开一篇介绍下,参考了不少的文章和资料。
LSM树是Log Structured Merge Trees
的简称(这里面的日志,不一定是指我们程序的日志,也是指一类以时间为其中维度的大批量的树)。在NoSQL数据库中有广泛的应用(比如LevelDB和HBASE等)。严格来讲并不是一种索引结构,是一种索引设计的整体架构性的东西。B+树作为关系型数据库常用的索引存储结构,本身挺优秀的,支持范围查询,随机访问,增删改查都支持的不错,只是如果在修改的时候,B+树的聚簇索引数据是存在叶子节点的,需要磁盘的随机读写,随机写的性能不够好。磁盘的随机读写的性能不好,顺序读写的性能可以达到和内存一个数量级别,对此的优化很容易想到采用批量顺序写替代磁盘的随机写。
LSM树适合的场景是写入的量很大,查询的量比较小,而且查询查询近期的数据,老的数据一般很少查询的场景,比较适合用LSM树。
Log Structured
采用日志追加的方式,而不是采用随机写,追加采用顺序写,所以性能好。另一个重要的思想是Merge Trees
, 数据写入的时候,先写入到内存的树中,可以是红黑树,也可能是B+树,有的采用跳表的内存数据结构。当内存中的树大小达到一定规模后,将内存中的树和磁盘中的树进行合并,这就是合并树的来源。数据开始写入到内存的时候,如果突然断电,或者系统异常,会导致数据丢失,为了防止数据丢失,采用WAL(Write Ahead Log 预写日志技术)将数据第一时间写入到磁盘文件中,由于是顺序写,所以性能上不用担心。在发生异常的时候,可以通过WAL文件进行数据恢复。
如上图所示,LSM树,整体由内存部分数据结构和磁盘上的两类文件组成。内存中的数据结构为memtable和immutable Memtable两部分组成,前一个可读可写,后一个是只读的,它们两是一样的数据结构。immutable Memtable 由于是只读的,可以在刷入到磁盘的时候不用加锁,提升性能。那么数据是如何读写的那?
写数据:
写请求来之后,写入到WAL中和内存的memtable中,memtable可以是红黑树或跳表等。
如果memtable满了,就新申请内存构建一个新的memtable,且将原来的memtable转为只读的immutable memtable
,适当时机刷到磁盘中,保存为一个SStable文件。
刷新到磁盘之后,就可以对WAL日志进行截断,这样防止WAL日志的无限扩大的问题。
immutable memtable
保存到磁盘中,如果直接和磁盘上的文件进行合并,因为内存中的数据少,磁盘中的很大,合并的数据很少,却占用了大量的IO磁盘,这肯定不行。所以像LevelDB
采用延迟合并的方式,每层满了之后,都会和下一层进行滚动合并。
读数据:
读数据的时候,先从内存中的memtable中读取。
如果没有,则到immutable memtable
中读取。
如果仍然未查询到数据,则到缓存中读取,缓存里面包括数据缓存和索引缓存。
如果仍然没有找到,则从Level 0
层开始查找,由于Level 0
层的SSTable没有合并,所以数据是有重合的,所以每个SSTable
都需要查找,Level 0
层的只有4MB(level DB)中,所以也比较快。
如果Level 0 层,没有找到数据,则下沉到下一层Level 1
层继续查找,一层以及以后层次SSTABLE文件是不重合的,所以可以通过快速定位到SSTable
文件中,进行查找,找到了或所有的层都查找完毕后返回。
SSTable
即Sorted String Table
,听着蛮高大上,其实就是排序的有序键值对集合,是存储在文件中的结构。我们保存哈希表为磁盘文件中,为了提升查询速度,一般还保存一个索引文件,保存key和offset的位置,通过key快速获得offset,再打开文件,直接定位到offset位置。但是哈希表没有顺序的,访问不同的key时候需要随机的磁盘访问,而且哈希表冲突的时候,需要复杂的处理逻辑,还无法支持范围查询。
简单改变下,我们按照key-value对的顺序按键排序,这种格式称为排序字符串表即SSTable。
此结构将数据部分和索引部分分离,在查询的时候,不需要将整个SSTable文件都读入到内存中。而是先读入索引部分,可以通过布隆过滤,快速判断要查的key是否存在,如果不存在,就不用再读数据部分了。
索引部分的Index Block
记录采用key:offset:size
形式,key
为每个Data Block
最小的key,offset
记录Data Block
的起始位置,size
为每个Data Block
的大小。每个Data Block
采用顺序存储的方式,我们可以方便进行二分查找。
如果每次都从 SSTable
中加载数据都需要从磁盘读取数据,性能比较差。所以LevelDB
中设计了内存的缓存区。缓存区包括两个部分,一部分为最近使用的Index Block
,另一个部分为Data Block
,这样如果我们搜索的索引和数据都在内存中,就不用从磁盘中读取,提升了查询性能。
首先为什么需要SSTable
合并那,那是因为,随着数据的增大,写的SSTable
文件越来越多,而且随着对key
的更新和删除,这种需要删除的数据越来越多,为了减少文件数量和清理无效数据,就需要进行compact
,即将多个SSTable
文件合并成一个SSTable
文件。
SSTable
按照分层滚动合并的方式,Level DB
的Level 0
层最多只能保存4个SSTable
,当Level 0
层满了之后,我们就将它们进行多路归并,合并后的文件就是Level 1
层的有序SSTable
文件,
除Level 0
层,SSTable
层的key的范围是不交叉的,这样查询的时候,就可以通过二分查找的方式进行快速查找了。Level 1
层的SSTable
就会从Level 1
层中轮询的方式选择一个SSTable
去和Level 2
层在此SSTable
的key
范围内的SSTable
进行文件合并,为了防止合并的占用的IO比较多,所以在生成SSTable
的会判断此文件和下一层多少个SSTable
有key
的重合,如果超过10个就停止写入,生成新的SSTable
文件。这样每次合并SSTable
文件消耗的IO也不至于太多。
合并的过程中,老的SSTable
文件,是不能删除的,所以就会同时存在老的SSTable
文件和新的SSTable
文件,导致同一份数据因为被compaction
,数据最多可能膨胀到原来的2倍。
不过数据会膨胀,由于在compaction
过程中,有可能同一份数据不断随着compaction
过程向更高层重复写入,有多少层有可能就写入多少次,IO几倍量的增加。
《核心索引20讲》 陈东 《数据密集型应用系统设计》 Martin Leppmann
我宁愿瞄准星星, 却击不中他们; 也不愿没有目标; 我宁愿去追逐目标, 却得不到它, 也不愿不曾追逐, 我宁愿去尝试却失败, 也不愿不曾尝试。 我不想活着的每一天 都在幻想如果我当初付出了更多努力 现在会怎样? 我会去放手追逐、 无论刀山火海, 我会去追逐我的命运! 你不能朝着你的梦想漫步, 你不能朝着梦想行走, 你要向它狂奔!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。