早期LSM树

为什么需要LSM树

B+树的数据都存储在叶子节点中,而叶子节点一般都存储在磁盘中。因此,每次插入的新数据都需要随机写入磁盘,而随机写入的性能非常慢。如果是一个日志系统,每秒钟要写入上千条甚至上万条数据,这样的磁盘操作代价会使得系统性能急剧下降,甚至无法使用。
针对这种频繁写入的场景,提出一种常见的设计思路和检索技术:LSM树。
LSM树是近年来许多火热的NoSQL数据库中使用的检索技术。

如何使用批量写入代替多次随机写入

操作系统对磁盘的读写是以块为单位的,我们也可以以块为单位写入,而不是每次插入一个数据都要随机写入磁盘。
LSM树根据上面这个思路设计这样一个机制:
当数据写入时,延迟写磁盘,将数据先存放在内存中的树里,进行常规的存储和查询。当内存中的树持续变大达到阈值时,再批量地以块为单位写入磁盘的树中。因此LSM树至少需要由两棵树组成,一棵是存储在内存中较小的C0树,另一课时存储在磁盘中较大的C1树。

C1存储在磁盘,因此我们可以直接使用B+树来生成。那对于全部存储在内存中的C0树,该如何生存?在数据都能加载在内存中的时候,B+树并不是最合适的选择,它的效率并不会更高。因此,C0树我们可以选择其他的数据结构来实现,比如平衡二叉树甚至跳表。我们先假设C0树也是一棵B+树。相比于普通的B+树,C0,C1树有一个特点就是所有叶子节点都是满的,因为C1树不需要支持随机写入了,我们完全可以等内存中的数据写满一个叶子节点之后,再批量写入磁盘,因此每个叶子节点都是满的,不需要预留空位来支持新数据的随机写入。

如何将内存数据与磁盘数据合并

解决了内存中数据备份的问题,我们就可以接着写入数据了。内存中C0树的大小是有上限的,当C0树被写满之后,怎么把它转化到磁盘的C1树上呢?这就涉及滚动合并的过程了。我们可以参考两个有序链表归并排序的过程,将C0树和C1树的所有叶子节点中存储的数据看作是两个有序链表,那滚动合并问题就变成了我们熟悉的两个有序链表的归并问题。

由于磁盘具有顺序读写效率高的特性,因此,为了提高C1树中节点的读写性能,除了根节点以外的节点都要尽可能地存放到连续的块中,让它们能作为一个整体单位来读写,这种包含多个节点的块就叫作多页块。

滚动归并:

  • 第一步,以多页块为单位,将C1树的当前叶子节点从前往后读入内存。读入内存的多页块,叫做清空块,意思是处理完以后会被清空。
  • 第二步,将C0树的叶子节点和清空块中的数据进行归并排序,把归并的结果写入内存的一个新块中,叫作填充块。
  • 第三步,如果填充块写满了,我们就要将填充块作为新的叶节点顺序写入磁盘。这个时候,如果C0树的叶子节点和清空块都没有遍历完,我们就继续遍历归并,将数据写入新的填充块。如果清空块遍历完了,我们就去C1树中顺序读取新的多页块,加载到清空块中。
  • 第四步,重复第三步,直到遍历完C0树和C1树的所有叶子节点=,并将所有的归并结果写入到磁盘。这个时候,我们可以同时删除C0树和C1树中被处理过的叶子节点。这样就完成了滚动归并的过程。
    在C0树到C1树的滚动归并过程中,你会看到,几乎所有的读写操作都是以多页块为单位,将多个叶子节点进行顺序读写的。而且,因为磁盘的顺序读写性能和内存是一个数量级的,这使得LSM树的性能得到了大幅度的提升。

LSM树是如何检索的?

因为同时存在C0和C1树,所以要查询一个key时,我们会先到C0树中查询。如果查询到了则直接返回,不用再去查询C1树了。
而且C0树会存储最新的一批数据,所以C0树中的数据一定会比C1树中的新。因此如果一个系统的检索主要是针对近期数据的,那么大部分数据我们都能在内存中查到,检索效率会非常高。
那如果我们在C0树种没有查询到key呢?那这个时候系统就会去磁盘中的C1树查询,在C1树种查到了,我们能直接返回吗?如果没有特殊处理的话其实不能。
比如如果一个数据已经被写入系统,并且我们也把它写入C1树了。但是,在最新的操作中,这个数据被删除了,那我们自然不会在C0树中查询到这个数据。可是它依然存在于C1树中。这种情况下,我们在C1树中检索到的就是过期的数据。既然是过期的数据,那为了不影响检索结果,我们能否从C1树中将这个数据删除呢?删除的思路没有错,但是我们不希望对C1树进行随机访问,这时我们该怎么处理?
我们依然可以采取延迟写入和批量操作的思路。对于被删除的数据,我们会将这些数据的key插入到C0树中,并且存入删除标志。如果C0树中已经存有这些数据,我们就将C0树中这些数据对应的key都加上删除标志。这样一来,当我们在C0树中查询时,如果查到了一个带着删除标志的key就直接返回查询失败,我们也就不用去查询C1树了。在滚动归并的时候,我们会查看数据在C0树中是否带有删除标志。如果有,滚动归并时将它放弃。这样C1树就能批量完成数据删除操作。(标记删除的数据时在merge合并的时候被物理删除的)

LSM树的使用场景

在写大于读的应用场景下,尤其在日志系统和监控系统这类应用中,我们可以使用基于LSM树的NoSQL数据库,这是比B+树更适合的技术方案。

LSM树具有以下三个特定:

  • 将索引分为内存和磁盘两个部分,并在内存达到阈值时启动树合并
  • 用批量写入代替随机写入,并且用预写日志WAL技术保证内存数据在系统崩溃后可以被恢复
  • 数据采用类似日志追加写的方式写入磁盘,以顺序写的方式提高写入效率

LSM树的这些特定,使得它相对于B+树,在写入性能上有大幅提升。所以,许多NoSQL系统都使用LSM树作为检索引擎,而且还对LSM树进行了优化以提升检索性能。

小总结

B+树是写入的时候就找好key的位置,读取的时候直接根据索引查找key的值
LSM是写入是可能一个key存在不同层的树上,读取的时候,需要合并key不同树上的值。
相当于B+树是写入时merge,LSM是读取时候merge

现代LSM树

早期LSM树:
实质就是利用内存,延迟写入磁盘的时机。
C0-tree 由于常驻内存,检索起来不会产生 IO,所以理论上,我们可以使用各种可用于高效索引的数据结构来存储数据,比如红黑树、跳表等等。但是因为内存成本高昂,能存储的数据必然有限,更大量的数据仍然需要存储在磁盘里。而磁盘中的 C1-tree 一般被实现为特殊的 B+ 树。数据的存储也会分为两个阶段,我们会一直先在内存中存储元素,直到内存中的数据到达一个阈值,我们会开始和 C1-tree 中的节点进行合并和覆写,过程和多路归并有点相似。因为我们可以决定写入磁盘的时机,所以完全可以保证 B+ 树的所有节点是满的,也就避免了许多单次的随机写操作。
现代LSM树包含三个部分:memtable、immutable memtable、SSTable
前两个在内存中,最后一个在磁盘中。我们会先把临时地数据写在memtable中,然后在合适的时机刷入磁盘的SSTable中。(WAL机制保证数据安全)

Memtable

内存中的数据结构,存储的是近期更新的记录值,类似原始LSM树,可用各种高效的数据结构实现,比如跳跃表,红黑树。

Immutable Table

在 Memtable 存储的元素到达一个数量级之后,我们就会把它固化成 immutable table,从字面上理解,就是不可变表。
memtable 的拷贝操作,拷贝过程需要时间,但同时我们的系统很可能在对外工作,所以创建副本可以很好的帮助我们避免读写冲突竞争,从而避免阻塞,提高性能。
顺序写入磁盘,SSTable不再发生变化

SSTable

SSTable不可变,因此对现有对象Key的更新不会覆盖现有的SSTable
当下我们有了内存中的有序结构,存储了近期的记录变更,如何把数据落盘,既要有磁盘顺序读写的优势,也能保证所写的格式便于改动也便于查询。
它是整个 LSM Tree 的核心,毕竟我们的大部分数据都是存储在磁盘上的,SSTable 就是在磁盘上做持久化的部分。本质其实很简单,就是一段段按照 key 有序排列的键值对。

SSTable 一般也会带上一个索引文件,值存储的是 key 所对应的 offset,加载到内存后,我们利用二分搜索可以很快查找出要访问的 key 的值。
最简单的持久化方式就是我们在磁盘上把内存中有序的键值对直接 dump 成一个个段,也就是 segment。后面存储的段和前面存储的段,key 可能是重复的,因为后面的段新一些,所以在有重复的时候,最靠后的段中的记录值,就是某个 key 最新的状态。
我们把内存中有序的数据结构比如红黑树中的记录,dump 到一段磁盘上的空间,然后按 segment 一段一段往后叠加

那在这样的存储下,检索数据的时候需要怎么做呢?很简单,就是从后面的段开始,往前遍历,看看是否有查找到目标 key,有的话就返回。由于从后往前遍历,我们第一次查询到 key 的时候,一定就是这个 key 对应的最新状态。

但是这样存储会有很多问题:

  • 数据冗余很大,随着时间的推移,磁盘上会有大量重复的键。
  • 其次我们需要遍历每个有序的 segment,查看数据是否存在。随着数据量增大,最坏情况下,要遍历的 segment 会非常多,整个系统的查询效率显然是惨不忍睹的。
    总之就是读的速度慢过头了。
    压缩数据:
    前提:每个 segment 都是有序的,那我们显然可以比较高效地对多段数据进行合并操作,就是“多路归并”的思路,一般,多路归并的程序我们会在后台不断运行,我们会不断地把多个老的 segment 合并成一个更长的、同样有序的 segment。
    在 SSTable 的主流实现里,我们会把不同的阶段被合并的 segment 放到不同的层中,并限制每一层数量,当某层 segment 超过一定数量,我们就会把它们删除,合并出一个更大的 segment 放入下一层。
    低层中的 segment 显然是更新的记录值,更高层的则是更老的记录值。

    在图的例子中可以看出来,我们合并 segment1、2、3 之后,在得到的 segment4 里,dog 的记录就只剩更新的 segment2 中的记录 84 了。这样我们的整个存储空间就不会无尽地膨胀,最高的一层,最多也就是占用历史以来所有出现过的 key 和对应的记录值这样数量级的空间,而存储这些是数据库本应做到的。

更新和删除数据:

将数据标记成一种特殊的状态。这种通过标记而不是真实移除数据的方法,在业务开发中其实也很常见,有时候我们称为 soft delete。在有些 ORM 库中会直接通过 deleteAt 字段,标记删除时间,来表示这个数据被删除了,想恢复这个数据的时候也很简单,直接将 deleteAt 置空即可。
在 LSM tree 中也是一样,我们把这个特殊的状态称为== tombstone,墓碑==,看图就非常清楚了。

查询的时候,如果我们先查到了 tombstone,就可以认为数据已经不复存在了。
更新数据的时候也是,把原来key的数据标记为墓碑,如下图

小总结

  • 内存上的部分,memtable、immutable memtable,比较简单,用通用的有序集合存储即可,跳表、红黑树都是非常不错的选择;
  • 磁盘上的数据结构,SSTable,也不复杂,就是一段段连续按 key 有序存储的段,唯一需要做的就是后台启动一个程序不断地进行多路归并,得到分层的有序存储结构。

每向下一级数据就会指数级增加

  • LSM树再内存中缓冲写入数据
  • 当缓冲区填满时,我们将其排序并作为不可变的SSTable刷新到磁盘
  • 随着更多缓冲区被刷新到磁盘,SSTable的数量也会增加
  • 会给读取带来问题,因此每次读取都必须搜索这些SSTable以执行查找。为了限制每次读取必须搜索的SSTable的数量,LSM树合并SSTable并在后台压缩它们。

优化查找

查找key,会在每个级别SSTable上执行搜索,排序数据搜索快,但遍历所有磁盘SSTable会消耗大量I/O。
许多系统在内存中保留一个汇总表,其中包含每个level的每个磁盘块的最小key和最大key范围,允许系统跳过对那些key不在范围内的磁盘块的搜索。
另一个可能代价高昂的问题就是查找不存在的key,那就得扫描所有level中符合条件的磁盘块,因此大多数系统在每一层level都有一个布隆过滤器来防止这周情况。

chatgpt给的总结

-LSM(Log-Structured Merge Tree)树是一种新型的索引结构,与传统的 B+ 树相比,具有以下优点:

  1. 高写入性能。LSM 树采用了日志结构存储方式,所有的写操作都追加到磁盘顺序写日志中,不需要进行随机写入操作,因此具有较高的写入性能。
  2. 空间利用率高。LSM 树使用了合并机制,将多个较小的数据文件合并成一个较大的文件,避免了 B+ 树中频繁分裂和合并导致的空间浪费问题。

​ 但是,LSM 树也存在一些缺点:

  1. 查询性能较差。由于 LSM 树中的数据需要通过多层文件进行合并才能达到可用状态,因此查询操作的性能相对较低。
  2. 不支持范围查询。由于 LSM 树中的数据文件是按照时间顺序排列的,因此不支持范围查询。
  3. 内存占用较高。由于 LSM 树需要维护多个数据文件,因此需要占用较多的内存空间。

只需这一篇博客就能完全弄懂LSM树相关推荐

  1. 鸿蒙关键技术研究,鸿蒙内核源码分析(静态链接篇) | 完整小项目看透静态链接过程 | 百篇博客分析HarmonyOS源码 | v54.02...

    百篇博客系列篇.本篇为: 下图是一个可执行文件编译,链接的过程. 本篇将通过一个完整的小工程来阐述ELF编译,链接过程,并分析.o和bin文件中各区,符号表之间的关系.从一个崭新的视角去看中间过程,阅 ...

  2. csapp实验摘选 I Data Lab ——小小菜下士的第一篇博客

    csapp实验摘选 I Data Lab --小小菜下士的第一篇博客 注:这是我的第一篇博客,试图在通往程序猿的路上踏出坚实的一步. --小小菜下士 实验来自: [读厚 CSAPP]I Data La ...

  3. 这篇博客记录廖雪峰python教程的习题(一)

    函数 函数的参数 "以下函数允许计算两个数的乘积,请稍加改造,变成可接收一个或多个数并计算乘积: def product(x,y): return x*y 由于是可以接收一个或多个参数,所以 ...

  4. 2020年我的第一篇博客日报

    博客日报缘由 谈起博客我们常想到CSDN.博客园.github等 谈起日报.周报我们自然想到了,工作工作工作... 但是谈起博客日报,那又是什么呢? "博客日报"不知道有没有这个词 ...

  5. 3个月时间,5名黑客找出苹果55个漏洞,赚了5万多美元,还写了篇博客记录全程

    大数据文摘出品 作者:刘俊寰.朱科锦.lin 昨天,翘首期待的iPhone12终于面世,不管是回归经典方框设计,还是首次推出小屏mini版,都让苹果玩家大呼过瘾. 不过,在今年这场别开生面的发布会之前 ...

  6. v74.01 鸿蒙内核源码分析(编码方式篇) | 机器指令是如何编码的 | 百篇博客分析OpenHarmony源码

    Python微信订餐小程序课程视频 https://blog.csdn.net/m0_56069948/article/details/122285951 Python实战量化交易理财系统 https ...

  7. Flutter基础篇(2)-- 老司机用一篇博客带你快速熟悉Dart语法

    版权声明:本文为博主原创文章,未经博主允许不得转载.https://www.jianshu.com/p/3d927a7bf020 转载请标明出处: https://www.jianshu.com/p/ ...

  8. 第一篇博客:那些年磕过的题之Fence Painting

    文章目录 前言 一.题目 英文原题 中文翻译 输入样例 输出样例 二.解题思路 三.代码 总结 前言 之前从没有过要写博客的想法,觉得会写博客的都是大佬,自己的水平还是太菜.这个学期末和大佬们一起学习 ...

  9. 孩子斗图老失败,多半是没看这篇博客

    2019独角兽企业重金招聘Python工程师标准>>> 群里跟人斗图的时候,有没有碰到这样的? 这时候没有图咋办?总不能真退群啊,但是又没图可以斗,于是乎~ 唉,尴尬不尴尬,斗图拼的 ...

最新文章

  1. Velocity魔法堂系列二:VTL语法详解
  2. imx6 uboot lvds clock
  3. 地形纹理Splatting技术(翻译)
  4. 迅速重载nginx脚本
  5. 如何优雅的激怒C/C++程序员
  6. VirtualBox Ubuntu个人配置
  7. 杀死 Oculus ,Facebook 改名 Meta ,是押注元宇宙还是“金蝉脱壳”?
  8. 通信系统的monte carl仿真_深国电公司电力物联仿真检测系统建成投运
  9. 东南大学2014计算机考研真题,2014年东南大学920信号数电考研真题(回忆版)
  10. 计算机自己创作游戏,说说自己做的数据库:我不爱玩电脑游戏,自己做数据库自己欣赏...
  11. 同济大学Openwrt路由器 ipv6教程(新手导向)
  12. 电流探头常见的三大类型
  13. 程序存储器 指令寄存器 程序计数器 地址寄存器
  14. echarts瀑布图_请问echarts中有没有瀑布图+折线图的使用方法?
  15. 卧室.餐厅.客厅要选择挂什么油画?
  16. pythorch 基本学习
  17. go 错误处理与测试
  18. Domain Adaptation and Adaptive Information Fusion for Object Detection on Foggy Days
  19. Ubuntu14.04下配置并使用MTK flash tool
  20. PLC模拟量采集在工业自动化控制中的应用

热门文章

  1. Linux常用命令——systemctl命令
  2. 自然语言分词词性标注指代
  3. Spring Security4 CSRF 如何关闭CSRF功能
  4. 程序设计阶段性总结报告一
  5. 《算法竞赛入门经典(第2版)》——学习记录
  6. [置顶] 程序员的奋斗史(二十八)——寒门再难出贵子?
  7. BarChart 柱状图 实例
  8. Integer类型的比较
  9. redis简单分布式锁实现 超简单的那种
  10. 音视频 SDP 添加码率