上一节介绍的LSM-tree,是已经被认可的日志存储的索引结构。这节我们会介绍更通用的一种索引类型:B-tree。
B-tree的概念
B-tree在1970年提出,并且已经广泛使用在所有的关系型和许多非关系型数据库中。下面我们通过和LSM-tree的对比介绍一下B-tree的特性:
- 相同:B-tree维护的是按key排序的键值对,实现高效的查找的范围查询;
- 不同:LSM-tree将数据库分为若干个变长的segments,而B-tree将数据库划分为定长的块或者页(fixed-size blocks or pages),这样更加接近于底层硬件,方便磁盘管理。
如下图可以直观的看到B-tree的查找过程,以一个页作为B-tree的顶端,按照key的值,依次查找key所在的范围的目标引用地址,直到叶子页(leaf page)。
每个节点带有的区间段个数称为分支因子,上图的分支因子为6。分支因子的大小由引用需要的存储空间和数据范围边界有关,一般都是几百个。
在更新B-tree的一个值时,同样需要需要查找该值在B-tree中的位置。如果页的存储空间不足以插入新值,则分裂为两个完整的页,同时更新上一级的页以负责新的key值范围。
该方法可以保证B-tree的平衡,每个点带有n个key的B-tree的深度为O(logn)。大多数的B-tree的深度为3-4,一个4层深并且分支因子为500的B-tree可以存储256TB的数据。
提升B-tree的可靠性
B-tree的修改,是需要修改并覆盖在磁盘上存储的对应的页文件,这和LSM-tree只在文件后追加,但不修改文件的方式是不同的。如果B-tree的修改引起了页文件的分裂,以及上层文件的修改,并且数据库崩溃导致操作没有全部完成,可能会导致索引文件的损坏。
为了使数据库能够抵抗这样的崩溃,一种普遍的做法是在修改B-tree的同时,将改动写入到磁盘文件中,称该文件为write-ahead log(WAL)。该文件用于在数据库崩溃时,恢复B-tree到正确的状态。
另外,在更新B-tree时,如果有多个线程同时进行写操作,可能会出现不一致的状态。因此使用latches(轻量级锁)来保护B-tree的数据结构。
B-tree的优化
B-tree经历了较长时间的发展,有很多的优化方式已经被提出,这里列举几点:
- 使用copy-on-write的机制替换WAL日志。对页文件的修改,是先在另一个位置写入新的版本,然后在树中指向该地址,该方法同样可用在并发场景中;
- 在页中,不保存整个key,只保存它的缩写,可以节省一部分空间;
- 将临近的页文件保存在磁盘的相邻位置,提高扫描和查询的速度;
- 在B-tree中增加额外的指针,比如在叶子页中增加到左右邻居的指针,可以更快的进行扫描;
- 借用日志结构的思想,减少磁盘寻址,比如fractal tree。
B-tree和LSM-tree的比较
- LSM-tree的优点:
- 建立B-tree需要额外写入WAL日志中,LSM-tree不需要;
- LSM-tree的写性能要优于B-tree,吞吐量更大和写磁盘次数更少;
- LSM-tree的压缩情况更好,占用的磁盘空间更少,B-tree的页空间浪费更多;
- LSM-tree的缺点:
- LSM-tree的压缩过程可能会影响正在进行的读写操作,因为它们共享磁盘I/O,所以LSM-tree的性能起伏更大,而B-tree的性能更稳定;
- LSM-tree的压缩速度可能会跟不上写入的速度,导致segments文件的数量越来越多,直至用完磁盘空间;
- LSM-tree中每个key在多个位置保存多个值,而B-tree则只有一个确定的位置保存值,因此B-tree更容易提供良好的事务性。
扩展阅读:其他索引方式
第二索引
前面提到的key-value型的索引,比如B-tree和LSM-tree,适合于做数据库的主键,因为主键是唯一确定数据库中的一行。那数据库的第二索引应该用怎么方式构建呢?
用B-tree这种唯一类型的索引也是可以的,但需要对重复值进行处理。一种处理方式是为每个值匹配的行创建一个列表;另一种处理方式是在每个key上增加一个唯一的行标识符。
索引的类型
从索引是否包含指向行的数据,可以将索引分为两大类型:
- clustered index:索引的值为数据库中对应行的数据,这种类型的索引可以节省一次读磁盘的时间,但需要额外的存储空间,以及事务性的处理等;
- nonclustered index:索引指向的是文件位置,数据库的内容都存储在堆文件中,这是比较常见的索引类型。
- covered index:折中了这两种索引方式的优缺点,将一部分列的内容作为索引的值,可提供更好的性能,其他列还需要从数据库文件中读取。
多列索引
多列索引的使用场景在使用多列进行查询时的提速。一种简单的实现方式是将索引中的每列的值连接之后作为索引的key。如果这些列之间存在一些逻辑关系,比如经度和纬度,红绿蓝等,可以使用R-tree创建索引。
全文搜索和模糊索引
Lucene是这类索引的代表,允许在一定的编辑误差内进行搜索。Lucene使用的类似SSTable的结构,内存中的索引是key中的字符自动修改后的有限状态,因此可以进行模糊搜索。
内存数据库
内存数据库指所有数据均保存在内存中的情况,比如Memcached,Redis,Couchbase等。内存数据库之所以性能更好,不完全是因为不需要进行磁盘读写。内存数据库由于不需要读写磁盘,可以避免在数据结构设计上的限制,从而得到更好的处理性能。并且能够处理更加复杂的数据结构,比如Redis中的优先级队列和集合等。