存储与检索 -- 为数据库提供动力的数据结构(哈希索引)

让我们从键值数据的索引开始。这不是你可以索引的唯一类型的数据,但它非常常见,而且它是构建更复杂索引的一个有用的模块。

键值存储与大多数编程语言中可以找到的dictionary类型非常相似,通常是作为散列表实现的。在许多算法教科书中都描述了哈希映射,因此我们不会详细讨论它们是如何工作的。既然我们已经有了内存数据结构的哈希映射,为什么不使用它们来索引磁盘上的数据呢?

假设我们的数据存储只包括追加记录到一个文件,就像前面的例子一样。然后,最简单的索引策略是:保存一个内存中的散列映射,其中每个键都映射到数据文件中的一个字节偏移位置,这个位置的值如图3-1所示可以被找到。当你在文件中添加新的键值对时,您还将更新散列映射以反映您刚刚编写的数据的偏移量(这既用于插入新键,也用于更新现有的键)。当您想查找一个值时,使用散列映射来查找数据文件中的偏移量,查找该位置,并读取该值。

图3-1 以类csv的格式存储键值对,并使用内存散列映射进行索引。

这听起来可能过于简单,但却是可行的方法。实际上,这就是Bitcask (Riak的默认存储引擎)所做的事情。Bitcask提供高性能的读写,根据要求,所有的键都可以放在可用的RAM中,因为哈希映射被完全保存在内存中。这些值可以使用比可用内存更多的空间,因为它们可以从磁盘上加载。如果数据文件的那一部分已经在文件系统缓存中,那么read不需要任何磁盘I/O。

像Bitcask这样的存储引擎非常适合于每个密钥的值经常更新的情况。例如,密钥可能是一只猫的视频URL,其值可能是它被播放的次数(每次有人点击播放按钮时递增)。在这种模式中,有很多写操作,但是没有太多不同的键——每个键有大量的写,但是将所有的键保存在内存中是可行的。

到目前为止,我们只讨论了追加记录到文件中,那么如何避免最终耗尽磁盘空间呢?一个好的解决方案是,当它达到一定的大小时,关闭一个segment文件,然后将其写入一个新的segment文件,从而将日志分割成一定大小的segment。然后我们可以对这些片段执行压缩,如图3-2所示。压缩意味着在日志中丢弃重复的键,并且只保留每个键的最新更新。

图3-2 键值更新日志的压缩(计算猫视频播放的次数),只保留每个键的最新值

此外,由于压缩常常使segment更小(假设一个键在一个segment内平均覆盖了几次),我们还可以同时合并多个segment,同时执行压缩,如图3-3所示。segment在写入后不会被修改,因此合并的segment被写入到一个新文件中。历史segment的合并和压缩可以在后台线程中完成,而在进行的过程中,我们仍然可以使用旧的segment文件继续服务读和写请求。在合并过程完成之后,我们将读取请求改为使用新的合并segment而不是旧的segment,然后删除旧的segment文件。

图3-3 执行压缩和segment合并

每个segment现在都有自己的内存哈希表,映射键来进行文件偏移。为了找到密钥的值,我们首先检查最近segment的散列映射;如果密钥不存在,我们将检查第二个最近的部分。。。合并进程保持了segment的数量很少,所以查找不需要检查很多散列映射。

这个简单的想法在实践中有很多细节。简而言之,真正实施的一些重要问题是:

文件格式
CSV不是日志的最佳格式。使用二进制格式(以字节为单位编码字符串的长度)和原始字符串(不需要转义)会更快更简单。

删除记录
如果您想删除一个键及其相关值,则必须将一个特殊的删除记录附加到数据文件(有时称为tombstone)。当segment被合并时,tombstone告诉合并进程放弃删除键的任何先前值。

崩溃恢复
如果数据库重新启动,内存中的散列映射就会丢失。原则上,你可以通过从头到尾读取整个segment文件来恢复每个segment的哈希映射,并注意在进行过程中每个键的最新值的偏移量。但是,如果segment文件很大,这可能需要很长时间,这会使服务重启很痛苦。Bitcask通过在磁盘上存储每个segment的散列映射的快照来加速恢复,这可以更快地加载到内存中。

部分文字记录
数据库可能在任何时候崩溃,包括将记录追加到日志的过程中。Bitcask文件包括校验和,允许检测和忽略该日志中损坏的部分。

并发控制
由于写入是严格按顺序添加到日志中的,所以一个常见的实现选择是只有一个写线程。数据文件segment是只允许追加,而且是不可变的,以便于可以通过多个线程并发地读取它们。

一个只允许追加的文件第一眼看上去很浪费:为什么不更新文件,用新值重写旧值?但是只允许追加的设计是好的,有几个原因:

  • 追加和segment合并是顺序写操作,通常比随机写快得多,特别是在magnetic spinning-disk硬盘上。在某种程度上,顺序写在基于闪存的固态硬盘(ssd)上也更可取。我们将在后面的“比较BTrees和LSM-Trees”中进一步讨论这个问题。
  • 如果segment文件是只允许追加的或不可变的,那么并发和崩溃恢复要简单得多。例如,您不必担心发生崩溃时当一个值被覆盖,会留下一个包含旧值的一部分,同时又将新值的一部分拼接在一起的文件。
  • 合并旧的segment可以避免数据文件随着时间的推移而变得支离破碎的问题。

然而,哈希表索引也有限制:

  • 哈希表必须适合内存,所以如果你有非常多的键,你就不走运了。原则上,您可以在磁盘上维护一个散列映射,但不幸的是,很难使磁盘上的哈希映射执行良好。它需要大量的随机访问I/O,而且当它很大时,再增长的代价就比较昂贵了,并且散列冲突也需要复杂的逻辑。
  • 范围查询无效。例如,您不能轻松地扫描kitty00000和kitty99999之间的所有key,您必须在散列映射中单独查找每个键。

在下一节中,我们将研究一个没有这些限制的索引结构。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容