键值对存储和关系型数据库
虽然一个关系型数据库支持多种类型的查询, 几乎所有查询都可以分解为三种类型的磁盘操作.
- 扫描所有的数据集(不使用索引)
- 点查询: 按指定key索引查询
- 范围查询: 按范围查询索引(索引是排序的)
数据库索引大多数关于范围查询和点查询, 而且很容易看出范围查询只是点查询的一个超集.如果我们提取数据库索引的功能, 那么创建键值存储就很简单了.但关键是数据库系统可以建立在KV存储之上.
在尝试关系型数据库之前, 我们将构建一个KV键值对存储, 但我们先探索一下我们的选项.
哈希表/散列表
哈希表/散列表在设计一个通用的kv存储时是首先被排除在外的.主要原因是排序-许多现实世界的应用需要分类和排序.
然而, 在专门的应用中可能会使用哈希表/散列表.使用哈希表的另一个头痛的问题是大小调整操作.初始调整大小复杂度在O(n), 会导致磁盘空间和IO的突然增加.是有可能做到增量地调整哈希表的, 但这会增加复杂度.
B-Trees B树
平衡二叉树可以在Ologn复杂度内被查询或更新, 还可以区间查询.一个B树大致是一个平衡的n叉树.为什么使用n叉树代替一个二叉树?有以下几种原因:
- 减少空间开销.二叉树的每个叶子节点都可以通过从父节点的一个指针到达, 而父节点可能也有自己的父节点. 平均下来, 每个叶子节点需要1-2个指针.这与B树相反.在b树中, 叶子节点中的多个数据共享一个父节点.n叉树也更短, 指针上浪费的空间更少.
- 在内存中更快.由于现代CPU内存缓存和其他因素, n叉树可以比二叉树更快, 即使它们的大O复杂度是相同的.
- 更少的磁盘IO.B树更短, 这意味着更少的磁盘搜索.磁盘IO的最小大小通常是内存页面的大小(可能是4k).操作系统将填满整个4k页空间, 即使你读取较小的大小. 如果我们利用4k页中的所有信息(通过选择至少一个页面的节点大小), 这是最优的.
我们在接下来的文章中使用B树, 但B树不是唯一的选择.
LSM树
Log-structured merge-tree日志结构merge树.下面是LSM树操作的高级描述.
如何查询
- 一个LSM树包含多级数据
- 每个级别被排序并分成多个文件
- 点查询从顶层开始, 如果没有找到关键字, 则继续搜索到下一层
- 范围查询合并所有级别的结果, 级别越高, 合并时优先级越高.
如何更新
- 更新一个键时, 首先从顶层将键插入到文件.
- 如果文件大小超过阈值, 将其与下一个级别合并.
- 文件大小阈值随着级别的增加呈指数增长, 这意味着数据量也呈指数增长.
让我们分析下这如何工作的.对于查询:
- 每个级别都是排序的, 键可以通过二分搜索找到, 范围查询只是顺序的文件IO, 它是高效的.
对于更新:
- 顶级文件的大小很小, 因此插入顶级文件只需要少量IO.
- 数据最终合并到较低的级别. 合并是顺序IO, 这是一个优势.
- 更高的级别会更频繁地触发合并, 但合并也更小.
- 将文件合并到较低级别时, 范围相交地较低级别文件将被合并地结果(可以是多个文件)替换. 我们可以看到为什么级别被分割成多个文件-减少合并的大小.
- 合并可以在后台完成.但是, 低级合并可能会突然导致高IO使用率, 从而降低系统性能.
读完本书后, 读者可以尝试使用lsm树来代替b树, 并比较b树和lsm树地优缺点.