key-value-store
请设计一个
这是一道频繁出现的题目,个人认为也是一道很好的题目,这题纵深非常深,内行的人可以讲的非常深。
首先讲两个术语,数据库和存储引擎。数据库往往是一个比较丰富完整的系统
目前开源的
LevelDB 整体结构
有一个反直觉的事情是,内存随机写甚至比硬盘的顺序读还要慢,磁盘随机写就更慢了,说明我们要避免随机写,最好设计成顺序写。因此好的
- MemTable。即内存中的
SSTable ,新数据会写入到这里,然后批量写入磁盘,以此提高写的吞吐量。 Log 文件。写MemTable 前会写Log 文件,即用WAL(Write Ahead Log) 方式记录日志,如果机器突然掉电,内存中的MemTable 丢失了,还可以通过日志恢复数据。WAL 日志是很多传统数据库例如MySQL 采用的技术,详细解释可以参考数据库如何用WAL 保证事务一致性?- 知乎专栏。- Immutable MemTable。内存中的
MemTable 达到指定的大小后,将不再接收新数据,同时会有新的MemTable 产生,新数据写入到这个新的MemTable 里,Immutable MemTable 随后会写入硬盘,变成一个SST 文件。 SSTable
文件。即硬盘上的SSTable ,文件尾部追加了一块索引,记录key->offset ,提高随机读的效率。SST 文件为Level 0 到Level N 多层,每一层包含多个SST 文件;单个SST 文件容量随层次增加成倍增长;Level0 的SST 文件由Immutable MemTable 直接Dump 产生,其他Level 的SST 文件由其上一层的文件和本层文件归并产生。Manifest 文件。Manifest 文件中记录SST 文件在不同Level 的分布,单个SST 文件的最大最小key ,以及其他一些LevelDB 需要的元信息。Current 文件。从上面的介绍可以看出,LevelDB 启动时的首要任务就是找到当前的Manifest ,而Manifest 可能有多个。Current 文件简单的记录了当前Manifest 的文件名。

- 首先
SST 文件尾部的索引要放在内存中,这样读索引就不需要一次磁盘IO 了 - 所有读要先查看
MemTable
,如果没有再查看内存中的索引 - 所有写操作只能写到
MemTable
, 因为SST 文件不可修改 - 定期把
Immutable MemTable
写入硬盘,成为SSTable
文件,同时新建一个MemTable
会继续接收新来的写操作 - 定期对
SSTable
文件进行合并 - 由于硬盘上的
SSTable
文件是不可修改的,那怎么更新和删除数据呢?对于更新操作,追加一个新的key-value 对到文件尾部,由于读SSTable
文件是从前向后读的,所以新数据会最先被读到;对于删除操作,追加“墓碑”值(tombstone) ,表示删除该key ,在定期合并SSTable
文件时丢弃这些key, 即可删除这些key 。
Manifest 文件

Log 文件
每个

SSTable

MemTable
前面我们介绍了
添加、更新和删除数据
- 将这个操作顺序追加到
log 文件末尾。尽管这是一个磁盘操作,但是文件的顺序写入效率还是跟高的,所以不会降低写入的速度 - 如果
log 文件写入成功,那么将这条key-value 记录插入到内存中MemTable 。
核心思想就是把写操作转换为顺序追加,从而提高了写的效率。
读取数据
读操作使用了如下几个手段进行优化:
- MemTable + SkipList
Binary Search( 通过manifest 文件) - 页缓存
- bloom filter
- 周期性分层合并