B+树
[ 简介 ]
B+树是一种专门针对磁盘存储而优化的N叉排序树。使用B+树组织数据可以较好地利用磁盘顺序访问较快而随机访问较慢的特点,它以树节点为单位存储在磁盘中,非叶子结点只存放索引,叶子结点存放数据并通过链表串联起来。
[ 结构]
[ 特性]
- 非叶子结点只存放索引,所有的数据最终存放在叶子结点 – – 每次查询的IO次数一样,速度稳定;降低树的深度,加快查找,减少磁盘io;
- 叶子结点索引有序 – – 对数据的排序有着更好的支持,便于范围查询。
- 适合读多写少的场景 – – 如果写入的数据比较离散,那么寻找写入位置时,子节点有很大可能性不会在内存中,最终会产生大量的随机写,性能下降。
[操作]
- 查询:根据索引访问B+树,找到对应的叶子结点即找到数据
- 插入:对于一个M阶树,遍历B+树,找到适合插入的节点,如果节点的key数目小于M,则可直接插入;若等于key,则需要将该结点裂变成两部分,一部分包含M/2+1个key,一部分包含M/2个key,同时将第M/2+1的key放到父节点中,再进行插 入;父节点超过M则继续裂变;如果插入的key值大于当前节点最大值,需要将经过的key值修改为自己的key,然后在进行插入和裂变
- 删除:遍历二叉树找到删除的key,若节点中key的数量大于M/2+1,则直接删除节点;如果删除的key为节点最大值或最小值,则需要向上回溯替换掉删除的key。其他情况则需要对节点进行合并或者裂变
[应用]
B+树被广泛应用在各种关系型数据库如MySQL、SQL Server、Oracle当中,这种结构考虑到磁盘的存储特性,可以有效的减少读写磁盘的次数,提高效率。
- mysql 的索引以索引文件(分为多个Index Page)的形式存储在磁盘上,每次索引的检索需要磁盘io操作,时间消耗较大所以每次都会从磁盘预读数据,每次读一个页大小数据(16k);
- mysql 维护的B+树索引每个节点的大小通常设置为一个页大小,每次访问一个节点只需要一次磁盘io,根节点常驻内存,一次检索只需要h-1次磁盘io(h为B+树深度)
- 不同存储引擎索引的实现方式也不同
Myisam:主键和辅佐索引的结构没有区别,叶子结点直接指向数据
innodb:数据文件本身就是主键索引页,辅助索引的叶子节点数据是主键key
LSM树
[ 简介 ]
LSM(Log-Structured Merge-Tree)日志结构合并树,实质是两个或以上的树或其他类似结构组成的集合。
相比起B+树,LSM树拥有更好的随机写性能。
LSM树的核心特点是利用顺序写来提高写性能,但因为分层(分为内存和文件两部分)的设计会稍微降低读性能,通过牺牲小部分读性能换来高性能写。
[结构]
三个重要组成部分
1、MemTable
MemTable是在维护在内存中的数据结构,用于保存最近更新的数据,会按照Key有序地组织这些数据。LSM树对于具体如何组织有序地组织数据并没有明确的数据结构定义,例如Hbase使跳表来保证内存中key的有序。
因为数据暂时保存在内存中,内存并不是可靠存储,如果断电会丢失数据,因此通常会通过WAL(Write-ahead logging,预写式日志)的方式来保证数据的可靠性。
2、Immutable MemTable
当 MemTable达到一定大小后,会转化成Immutable MemTable。Immutable MemTable是将转MemTable变为SSTable的一种中间状态,随后会flush到磁盘当中。
3、SSTable(Sorted String Table)
有序键值对集合,是LSM树在磁盘中的数据结构。为了加快SSTable的读取,可以通过建立key的索引以及布隆过滤器来加快key的查找。
LSM树会将所有的数据插入、修改、删除等操作记录保存在内存之中,当此类操作达到一定的数据量后,再批量地顺序写入到磁盘当中。这与B+树不同,B+树数据的更新会直接在原数据所在处修改对应的值,但是LSM数的数据更新是日志式的,当一条数据更新是直接append一条更新记录完成的。这样设计的目的就是为了顺序写,不断地将Immutable MemTable flush到持久化存储即可,而不用去修改之前的SSTable中的key,保证了顺序写。
[操作]
-
插入:先预写日志WAL且同时写memtable。memtable写满后会自动转换成不可变的immutable memtable并flush到磁盘,形成L0级sstable文件(同时生成新的memtable)。sstable内部存储的数据是按key来排序的。
L0层的SST是没有进行合并(compation)的,所以key会有重叠;其他层会进行合并,不会有重叠key
-
查询:当收到一个读请求的时候,会直接先在内存里面查询,如果查询到就返回。如果没有查询到就会依次下沉,直到把所有的Level层查询一遍得到最终结果。
-
删除:删除数据时,LSM 树将内存中节点标记为「已删除」状态,而不直接到磁盘中删除。等到下次合并操作时,才将数据真正删除。如果数据在磁盘里,去磁盘里查找并删除成本太高了。所以直接在内存里插入一个一样的数据,并标记为「已删除」。等到下次合并的时候,遇到已删除的节点,则直接删除即可。
- 合并(compation)
合并主要有两种策略:size-tiered和 leveled, 主要是在读放大,写放大,空间放大当中做出取舍
-
- size-tiered:适合write-intensive workload,有较低的写放大,缺点是读放大和空间放大较高
size-tiered策略保证每层的SStable大小相近或相同,达到一定的数量后(比如图中数量为4),会合并这些SSTable,合并之后写到下一层成为更大的SSTable,当层数达到一定的数量,最底层的SSTable会变得非常大。
而且size-tiered策略会导致空间放大比较严重,即使是同一层的SSTable,key值的记录也会存在多份,只有该层执行compact操作时,才会消除无效记录。而range查询需要合并所有层的结果,runs越多,读放大越严重。
-
- leveled:降低了空间放大和读放大,比较适合read-intensive workload,写放大较高
leveled策略的每层由多个sstable组成一个有序的的集合,SSTable之间保持有序,key不相交。
每层的数据大小到达上限后与下一层有重复范围的sst进行合并,生成的文件放在下一层
可见,leveled compaction与size-tiered compaction相比,每层中SST的key区间都是不相交的,重复key减少了,所以很大程度上缓解了空间放大的问题。
leveled策略的问题是写放大,一次写操作可能带来多次的向下合并
-
- 混合compaction:如 level 0 用size-tiered,其他层用leveled
[ 特性 ]
LSM具有批量特性,存储延迟 写在内存,达到阈值才刷入内存
适合用与写多读少场景,大多NoSQL数据库核心思想都是基于LSM来做的,代表数据库:nessDB、leveldb、hbase等
LSM树需要对读放大,写放大,空间放大进行取舍
解决读放大可以通过缓存,布隆过滤器缓解
解决空间放大可以通过压缩,compaction合并缓解