第三章 数据与索引

最后更新于:2022-04-02 04:18:27

[TOC] ## 最简单的kv库 ``` #!/bin/bash db_set () { echo "$1,$2" >> database } db_get () { grep "^$1," database | sed -e "s/^$1,//" | tail -n 1 } ``` ``` $ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}' $ $ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}' $ db_get 42 {"name":"San Francisco","attractions":["Golden Gate Bridge"]} ``` 因为没有索引,所以查询复杂度为 O(n), ## 哈希索引 最简单的索引策略就是:保留一个内存中的哈希映射,其中每个键都映射到一个数据文件中的字节偏移量,指明了可以找到对应值的位置 当你将新的键值对追加写入文件中时,还要更新散列映射,以反映刚刚写入的数据的偏移量(这同时适用于插入新键与更新现有键) ***文件格式*** ​ CSV不是日志的最佳格式。使用二进制格式更快,更简单,首先以字节为单位对字符串的长度进行编码,然后使用原始字符串(不需要转义)。 ***删除记录*** 如果要删除一个键及其关联的值,则必须在数据文件(有时称为逻辑删除)中附加一个特殊的删除记录。当日志段被合并时,逻辑删除告诉合并过程放弃删除键的任何以前的值。 ***崩溃恢复*** 如果数据库重新启动,则内存散列映射将丢失。原则上,您可以通过从头到尾读取整个段文件并在每次按键时注意每个键的最近值的偏移量来恢复每个段的哈希映射。但是,如果段文件很大,这可能需要很长时间,这将使服务器重新启动痛苦。 Bitcask通过存储加速恢复磁盘上每个段的哈希映射的快照,可以更快地加载到内存中。 ***部分写入记录*** 数据库可能随时崩溃,包括将记录附加到日志中途。 Bitcask文件包含校验和,允许检测和忽略日志的这些损坏部分 ***并发控制*** 由于写操作是以严格顺序的顺序附加到日志中的,所以常见的实现选择是只有一个写入器线程。数据文件段是附加的,或者是不可变的,所以它们可以被多个线程同时读取 使用追加日志而不是写入文件是因为 - 追加和分段合并是顺序写入操作,通常比随机写入快得多,尤其是在磁盘旋转硬盘上。在某种程度上,顺序写入在基于闪存的 固态硬盘(SSD) 上也是优选的 - 如果段文件是附加的或不可变的,并发和崩溃恢复就简单多了。例如,您不必担心在覆盖值时发生崩溃的情况,而将包含旧值和新值的一部分的文件保留在一起。 - 合并旧段可以避免数据文件随着时间的推移而分散的问题。 哈希表索引也有局限性: * 散列表必须能放进内存 * 范围查询效率不高。例如,您无法轻松扫描kitty00000和kitty99999之间的所有键——您必须在散列映射中单独查找每个键。 ## SSTables和LSM树 LSM 树使用**排序字符串表**(Sorted Strings Table 简称 SSTable)的格式保存到磁盘。如名称所示,SSTables 是一种用于存储键-值对的格式,其中键按有序排列。SSTable 将由多个名为段(*Segments*)的有序文件组成。一旦将这些数据段写入磁盘后,就是不可变的。简化示例如下 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/d2/47/d247c8aca94b830e60a917abc72019fa_1290x704.png) ### 性能优化 当查找数据库中不存在的键时,LSM树算法可能会很慢:您必须检查内存表,然后将这些段一直回到最老的(可能必须从磁盘读取每一个),然后才能确定键不存在。为了优化这种访问,存储引擎通常使用额外的Bloom过滤器 ## B树 我们前面看到的日志结构索引将数据库分解为可变大小的段,通常是几兆字节或更大的大小,并且总是按顺序编写段。相比之下,B树将数据库分解成固定大小的块或页面,传统上大小为4KB(有时会更大),并且一次只能读取或写入一个页面。这种设计更接近于底层硬件,因为磁盘也被安排在固定大小的块中 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/e1/61/e161e5568e8c32ed3a396360fa1c736f_1150x646.png) 图3-6 使用B树索引查找一个键 如果要更新B树中现有键的值,则搜索包含该键的叶页,更改该页中的值,并将该页写回到磁盘(对该页的任何引用保持有效) 。如果你想添加一个新的键,你需要找到其范围包含新键的页面,并将其添加到该页面。如果页面中没有足够的可用空间容纳新键,则将其分成两个半满页面,并更新父页面以解释键范围的新分区 如果因为插入导致页面过度而拆分页面,则需要编写已拆分的两个页面,并覆盖其父页面以更新对两个子页面的引用。这是一个危险的操作,因为如果数据库在仅有一些页面被写入后崩溃,那么最终将导致一个损坏的索引(例如,可能有一个孤儿页面不是任何父项的子项) 为了使数据库对崩溃具有韧性,B树实现通常会带有一个额外的磁盘数据结构:**预写式日志**(WAL, write-ahead-log)(也称为**重做日志**(redo log))。这是一个仅追加的文件,每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时,这个日志被用来使B树恢复到一致的状态 ### 比较B树和LSM树 尽管B树实现通常比LSM树实现更成熟,但LSM树由于其性能特点也非常有趣。根据经验,通常LSM树的写入速度更快,而B树的读取速度更快 ## LSM树 ### 优点 B树索引必须至少两次写入每一段数据:一次写入预先写入日志,一次写入树页面本身(也许再次分页)。即使在该页面中只有几个字节发生了变化,也需要一次编写整个页面的开销。有些存储引擎甚至会覆盖同一个页面两次,以免在电源故障的情况下导致页面部分更新 由于反复压缩和合并SSTables,日志结构索引也会重写数据。这种影响 —— 在数据库的生命周期中写入数据库导致对磁盘的多次写入 —— 被称为**写放大(write amplification)** ### 缺点 日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试逐步执行压缩而不影响并发访问,但是磁盘资源有限,所以很容易发生请求需要等待而磁盘完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是在更高百分比的情况下 ## 全文搜索和模糊索引 全文搜索引擎通常允许搜索一个单词以扩展为包括该单词的同义词,忽略单词的语法变体,并且搜索在相同文档中彼此靠近的单词的出现,并且支持各种其他功能取决于文本的语言分析 ## 在内存中存储一切 诸如VoltDB,MemSQL和Oracle TimesTen等产品是具有关系模型的内存数据库 反直觉的是,内存数据库的性能优势并不是因为它们不需要从磁盘读取的事实。即使是基于磁盘的存储引擎也可能永远不需要从磁盘读取,因为操作系统缓存最近在内存中使用了磁盘块。相反,它们更快的原因在于省去了将内存数据结构编码为磁盘数据结构的开销
';