1. 数据库的本质
数据库的本质就是帮助我们存储和获取数据个中间件。 最简单的数据库可以通过下面两个脚本实现。
- set.sh 存储
[qa@qaserver1 db]$cat set.sh
#!/bin/bash
echo "$1,$2" >> database
- get.sh 获取
[qa@qaserver1 db]$cat get.sh
#!/bin/bash
cat database | grep "$1," | tail -n 1 | awk -F, '{print $2}'
这个实现写入十分高效,所有的写入都是顺序写入,查询性能就比较糟糕,时间复杂度O(n),每次都是全部扫描。
数据库设计者们通过修改数据的存储结构,或者在数据外额外的维护一份元数据来加速查询过程,这一份额外的元数据被称为索引,最常见的索引是Hash索引、LSM-Tree、 B+Tree。
2. Hash索引
以我们上面举例的最简单Key-Value数据库为例,最直观的索引结构就是Hash索引,在内存中维护一个HashMap,HashMap的Key就是数据库的Key, HashMap的Value是文件内位置(offset)。
有了Hash索引后,每次我们可以通Key找到记录地址,seek到特定位置,直接读取数据返回。 每次写入数据时,需要同时更新Hash索引。
Hash索引有两个明显的缺陷:
- 内存需要能过放下索引的整个HashMap
- Range Scan效率低下
3. Segment
数据文件如果只是不断的追加,即使数据库系统不间断正常运行,文件会越来越大,文件里的无用信息越来越多(一个Key的值修改了N次,文件里记录了N条,只有最后一条有意义),如果从文件从新构建索引,耗时也会很长。
于是数据库设计者们想到了将数据文件切分,切分的每一段被称为一个Segment,只有当前Segment会有数据Append动作,历史Segment都可以认为是固定的。
那我们就可以启动后台线程对历史Segment做合并压缩(Compaction),一个Key的多次操作只保留最后一条,之前已经标记为删除的数据移除。
每个Segment都有对应的Hash索引,查找特定Key时,我们需要依次查询Segment对应的Hash索引。
这类存储引擎称为 Append-Only Log-Structed。 它们的优点是:
- 永远只做顺序写入,写入效率极高
确定也很明显:
- Hash索引庞大,需要存入到内存,然而内存大小永远是受限的
- 一个Key可能在多个Segment里,需要遍历每个Segment(至少检查每个Segment索引是否包含Key)
4. SSTable 和 LSM-Tree
前面提到用Hash索引是需要将每个Key保存到内存中,但是内存可能无法放下所有的Key。 SSTable完美解决了这个问题, SSTable全称Sorted String Table, 要求每个Merged-Segment里Key只出现一次,且Segment内Key是有序的。
相比之前简单的拆分Segment,SSTable有如下优势:
- SSTable内的Segment是有序的,所以合并Segment要高效的多,也不需要完整的加载两个Segment排序然后合并
- Segment有序,我们的索引里不需要完整的保存索引Key,而至少维护一个稀疏索引
- Segment有效,在Range-Scan的只需要顺序扫描,十分高效
那如何做到Segment内部有序呢? 因为数据写入会先经过内存,我们可以在内存维护一个红黑树(Red-Black Tree)或者平衡二叉树(AVL Tree)。
写入SSTable的工作流程:
- 写入Write-Ahead-Log,用于避免宕机导致Memtable丢失,通过重做日志恢复Memtable
- 数据插入到平衡二叉树中,内存中的这棵树我们称为Memtable
- 当Memtable超过一定阈值后,将Memtable Flush到磁盘,输出的文件被称为SSTable,也是之前说的一个Segment
- 清除已经写入SSTable的Memtable数据,比如新生成一个Memtable接收新数据,老的Memtable用于写入SSTable的,写完后丢弃
- 记录已经固化的Write-Ahead-Log的Position,下次重做日志从这个Position开始
读取SSTable的工作流程:
- 查询每一个SSTable的索引,确认是否包含给定Key,包含则读取对应Value
- 后面读取的SSTable的Value值,覆盖之前的值
- 读取当前正在写入Memtable,如果读到覆盖之前的值
==LSM-Tree的三大核心主键就是: Write-Ahead-Log、 Memtable 、 SSTable, 上面我们讲的其实就是LSM-Tree的核心思想。==
LSM-Tree已经被广泛使用, 如LevelDB、 RocksDB、 Cassandra 、 HBase都采用这个核心架构。 SSTable的概念也被广泛借鉴,如ClickHouse、 Palo里都有相关概念。
5. 总结
LSM-Tree的优点:
- 高吞吐的写
- 可以高效的压缩,更节省磁盘(B-Tree一般会为Page的分裂预留一些空间)
LSM-Tree的缺点:
- Compaction会影响正常数据的读取,读取性能略有影响。
- 数据量越大,Compaction需要的磁盘带宽就越多。
- B-Tree中Key只在一个Page中,LSM-tree可能出现在多个Segment中,影响读取性能,想要事务加锁也更困难一些。
参考文档