LSM 不是 老色批
LSM 树 (Log-Structured Merge-Tree) 即日志结构合并树。其实它并不属于一个具体的数据结构,它更多是一种数据结构的设计思想。大多 NoSQL 数据库核心思想都是基于 LSM 来做的,只是具体的实现不同。
何为 LSM 树
由于磁盘 IO 的开销是数据库效率瓶颈之一,因此产生了很多减少磁盘 IO 的方案。而 LSM 树就是为了解决频繁磁盘读写的方案之一。
使用 B 树之类的多路查找树索引数据时,由于插入数据会导致进行再平衡,使插入的效率变低。同时,由于 B 树的叶子节点之间的数据可能是相邻的,但是由于处于不同子树,导致虽然是数值是连续的,但是物理不连续,造成了大量的随机读写,使磁盘 IO 效率变低,
LSM 提供了一种设计思想,同时使用内存和磁盘存储数据。并通过一定的策略,将内存的数据按顺序刷到磁盘中。这样数据在磁盘中从逻辑和物理上都是有序存储的。
LSM 在数据写入时,不直接写到磁盘中,而是在内存中保留一段时间,并使用 WAL(Write-ahead logging,预写式日志)来保证数据不丢失,类似 MySQL 中的 Redo Log。这使得 LSM 可以承受很高的写请求。
LSM 的基本原理
LSM 中包含三个部分(以 RocksDB 为例,不同的存储引擎可能设计思想相同,具体实现方式不同)
- MemTable 内存中的数据结构
- Immutable MemTable 即将转为 SSTable 的中间状态
- SSTable(Sorted String Table) LSM 树在磁盘中的数据结构
新的数据在内存中以 MemTable 的形式组织起来,当达到一定容量后,转为 Immutable MemTable ,使其与 SSTable 中的数据进行合并。这时再有新的数据插入时,则生成新的 MemTable 进行记录。Immutable MemTable 与 SSTable 合并后,按照一定顺序存储到磁盘中。
当查询数据时,先从 MemTable 中查找,如果查找不到,则到磁盘中查找。所以 LSM 树的查询成本很高。
由于使用 WAL 日志记录数据的操作,所以当内存中的数据由于意外丢失后,可以通过日志重建内存数据。WAL 日志虽然存在了磁盘,但是也是按顺序追加存储,所以效率很高,不影响插入数据的效率。
插入与合并
在实际的应用中,内存中会有很多 MemTable ,而磁盘中也会有很多 SSTable,我们假设内存中只有一个 MemTable,称之为 C0,磁盘中也只有一个 SSTable ,称之为 C1。
在合并的过程中,有两个块结构。
- Emptying Block 存放 C1 中未合并的数据
- Filling Block 存放合并后的数据,装满数据后,将数据追加到磁盘中
插入数据时,先将数据放到内存组织起来,等到合适的时机,将内存的数据和磁盘的数据进行合并,并顺序写到磁盘上。
下面将以大量的图文的方式,简述插入、合并、追加的过程。
插入数据 1
现在日志里面记录插入日志
然后将数据 1 放入 C0
插入数据 2
插入数据 3,C0 以 B 树的形式存储(用其他数据结构也可以)
接着插入其他一坨数据
这时假定触发了合并,那么我们首先将 C1 的数据加载到 Emptying Block 中,但是由于 C1 没有数据,所以 Emptying Block 数据为空。直接将 C0 中的数据按顺序放到 Filling Block 中即可。
将节点 1 放入 Filling Block
节点 1 放到 Filling Block 后,则将 C0 中的节点 1 删除
再放入节点 2,由于删除节点 2 后,导致不平衡,需要将树重新平衡。
节点 3 替换到 节点 2的位置,保持树的平衡
将节点 3 放入 Filling Block 后,将树「左旋」保持树的平衡 (左旋的方法参考平衡二叉树的介绍)
当 Filling Block 到达容量限制后,将 Filling Block 的内容追加到 C1 中
然后我们再插入数据 4 9 10
当现在又要进行合并的时候,则需要将 C1 的数据放到 Emptying Block 里面,然后与 C0 进行合并。
将 Emptying Block 的数据与 C0 的数据按顺序合并到 Filling Block。Emptying Block 的数据合并到 Filling Block 后,将 Emptying Block 的数据删除。
Filling Block 的数据满了之后,追加到 C1,删除原来的数据
然后再继续合并
Filling Block 满了之后,在追加到 C1 上,并构建 B 树 (或其他索引结构)
这样通过不停的插入、合并、追加,最终构成了 LSM 树的设计思想。
查找数据
查找数据时,先从内存中进行查找,如果内存中没有,则到磁盘中查找。
查找 9,在 内存中可以命中
查找 7,在内存中找不到,再去磁盘中查找
删除数据
删除数据时,LSM 树将节点标记为「已删除」状态,而不直接删除。等到下次合并操作时,才将数据真正删除。
如果数据在内存里,则直接标记为「已删除」
如果数据在磁盘里,去磁盘里查找并删除成本太高了。所以直接在内存里插入一个一样的数据,并标记为「已删除」。
等到下次 合并 的时候,遇到已删除的节点,则直接删除即可。
总结
LSM 利用内存的高性能 IO,提高了写的性能。利用合并策略,将内存的数据与磁盘的数据进行合并排序并持久化。通过 WAL 来防止内存数据因为断点等意外丢失。软硬兼施,使得 LSM 拥有了良好的读写性能。
由于 LSM 查询时,先查询内存,再查询磁盘,而且内存和磁盘不一定只有一棵树,往往存在众多树结构,这导致查询的性能远远不如 B+ 树等传统关系型存储引擎快。
LSM 树在写场景下,远超 B+ 树,而在读场景下,远远落后 B+ 树。所以 LSM 适合应用在写多读少的场景。
代表数据库:NessDB、Leveldb、HBase等
核心思想的核心就是放弃部分读能力,换取写入的最大化能力,放弃磁盘读性能来换取写的顺序性。极端的说,基于 LSM 树实现的 HBase 的写性能比 MySQL 高了一个数量级,读性能低了一个数量级。