最简单的数据库
从最基本的层面来看,数据库的核心功能应该只有两个:存储数据并在我们查询时返回那些数据
在我们本文的讨论过程中我们可以假设一个最简单的数据引擎:
我们在磁盘中存储一个db文件,任何写入操作都会在该文件末尾进行追加(也就是写入相同键时不会覆盖之前的值)。至于读取,我们需要返回最后一个键对应的值。
db_set(){
echo "$1,$2">>database
}
db_get(){
grep "^$1," database| sed -e "s/^$1,//"|tail -n 1
}
这种数据库的写入性能应该是无所挑剔的,但问题也很明显,我们的读取操作都是o(n)的
哈希表索引
为了高效查找数据库中特定键的对应值,我们需要额外维护一个新的数据结构:索引。所有索引的基本想法都是保留额外的元数据,作为‘路标’帮助定位想要的数据。
索引不会影响数据内容,只会影响查询性能,对于写入,任何类型的索引都会降低写入速度,因为我们都需要在写入时更新索引
Key-Value存储与大多数编程语言所内置的字典结构都十分相似,通常采用hash map来实现,这里我们不做详细描述。采用hash表索引的最简单策略就是:保存内存中的hashmap,把每个键一一映射到数据文件中特定的字节偏移量。
理论上应该没有问题,但可能需要进一步进行优化。
- 对文件进行顺序追加必然导致大量数据冗余,如何解决?
一般的解决方式是用后台线程,对文件进行定时的压缩合并,对于相同的键,只保留最新的值。 - 那么删除数据呢?
删除数据在对应的数据上标记一个墓碑,在进行合并日志段时,一旦发现标记墓碑,就会丢弃该记录。 - crashSafe:
由于是对文件进行追加写,内存中的索引文件会丢失但是具体的数据文件不会。我们可以通过扫描磁盘文件重新建立索引,但由于是O(n)操作,在文件过大时会消耗大量时间。
既然采用了hash索引为甚么不在更新数据时进行原地更新覆盖,而是继续追加写呢?
- 顺序写要比随机写性能高很多,在旋转式磁盘上很容易理解这一点,实际上SSD上也适用
- 如果覆盖,我们无法在崩溃恢复时判断当前值是否是最新值
- 避免出现碎片化问题
hash索引的问题
- hash索引要求要能将所有的索引加载进内存。尽管理论上hash索引一部分存放在磁盘中仍然可以工作,但是磁盘上的hashmap表现很差,因为访问hash索引存在大量的随机访问,不适用局部性原理,可能会导致磁盘和内存的不断替换
- 区域查询效率太低,由于索引不是顺序的,区域查询只能使用逐个查找的方式查询每一个键
SSTable和LSMTree
Sorted String Table(SSTable)–是存储,处理和交换数据集的最流行的输出之一。正如名字本身所包含的意思一样,SSTable是一个简单的抽象,用来高效地存储大量的键-值对数据,同时做了优化来实现顺序读/写操作的高吞吐量。
- SStable在功能上只是对hash加入了一个按键值排序。
- 由于是排序数据,我们不必像hash索引一样记录所有数据的位置,索引可以是稀疏的,例如对于段文件中的上千条数据,只保留一个索引
例如:对于数据键1~100000,我们每隔1000个数据保留一个索引,当查找5500时,对应索引应该是5000,之后向后查找500条(或者使用二分)。
使用方法似乎很好理解,麻烦的是构建和维护SStable。SStable基本工作流程是:
- 写入时将内容插入内存中的排序数据结构(例如红黑树)
- 当内存大于一定阈值时将其写入磁盘,作为磁盘的一部分
- 如果接收到新的读取请求,要先尝试在内存中读取,其次是磁盘的最新排序部分,然后是次新的,依次类推
- 后台进程周期性将各个排序部分归并,并且出现相同数据时只会保留最新值。
如果使用sstable,先进行内存写,所以我们也要考虑日志系统,类似innodb里redolog方式来保证持久性。
LSM-Tree 全名叫Log-Structured Merge Tree,最早建立在日志结构文件之上,现在基于合并和压缩排序文件原理的存储引擎都统称为LSM存储引擎。我们通常把LSM看成一种思想:保存在后台合并的一系列SStable
这种思想简单且有效:
- 即使数据集远大于内存,LSM-tree也能正常工作
- 由于键值有序,范围查询相比于hash表有很大优势
- 由于写入是顺序的(归并是后台线程在空闲时间做的)LSM-tree可以提供非常高的写入吞吐量
优化:在LSM系统中查找一个不存在的键时会导致查询时间长,因为要从最新的数据一直往前查找,所以lsm一般会使用布隆过滤器进行优化