Data Warehouse

(DDIA)数据存储与检索(一)

2019-02-14  本文已影响5人  雨钓Moowei

翻译《Designing Data-Intensive Applications》
作者:Martin Kleppmann
译者:雨钓(有增改)

一、Storage And Retrieval

一个数据库最基本的要具有两个功能:当你给它一些数据的时候它可以帮你存储数据,之后当你需要这些数据时,他可以返回给你所需要的数据。

你(应用程序开发人员)向数据库提供固定格式的数据,稍后你就可以再次请求获取这些数据。 在本章中,我们将从数据库的角度讨论以下问题:数据库如何存储我们所给出的数据,以及当我们需要这些数据时,我们如何再次从数据库里找到它,即数据库内部是如何存储和检索数据的。

你可能会问,为什么作为一个应用开发人员需要关心数据库的存储和内部检索实现?毕竟我们基本不可能从头开始去实现自己的数据存储引擎,因为已经用众多可以直接使用的方案。此外针对事务性工作负载进行优化的存储引擎和为分析型工作负载进行优化的存储引擎之间存在很大的差别。

首先我们将从这一章开始讨论存储引擎,这些存储引擎可能在你熟悉的各种数据库中使用:传统的关系数据库,以及大多数所谓的No SQL数据库。我们将主要介绍这两个家族的存储引擎。

二、Data Structures That Power Your Database

一个最简单的存储引擎的实现如下:

#!/bin/bash
db_set () {
    echo "$1,$2" >> database
}
db_get () {
    grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}

这两个函数实现一个键值存储。您可以调用db_set key,它将在数据库中存储key和value。key和value可以是(几乎)任何你想要的东西——例如,value可以是一个JSON文档。之后你可以调用db_get key,查找与该特定key关联的最新值并返回它。

底层存储格式非常简单:一个文本文件,其中每一行都包含了一个key-value对,用逗号分隔(大致像CSV文件一样)。每个对db_set的调用都会将数据追加到文件的末尾,因此,如果你更新了一个key,那么这个key的旧版本不会被覆盖——你需要查看该文件中这个key所对应的所有历史value值,并找到最新的value。

我们的db_set函数实际上具有相当好的性能,因为它非常简单,同时将数据追加到文件的方式通常非常高效。与db_set函数类似,许多数据库在内部使用类似的日志格式,这是一个仅附加(Append only)的数据文件。

真正的数据库有更多的问题需要处理(比如并发控制,磁盘空间回收,这样日志就不会永远增长,处理错误等等),但是基本原理是一样的。日志格式的数据存储非常有用,我们将在之后的其余部分中多次遇到它们。

( “日志”一词通常用来指应用程序日志,其中应用程序输出用于描述正在发生的事情的文本。在这本书中,日志具有更一般的意义:一个附加的记录序列。它不需要是人类可读的;它可能是只用于其他程序的读取的二进制的文件。)

另一方面,如果数据库中有大量的记录,那么db_get函数的性能会很糟糕。每次你想要查找一个key时,db_get将从头到尾扫描整个数据库文件,查找key对应的所有记录。在算法术语中,查找的成本是O(n):如果你的数据库中的数据量增加时,那么查找所需的时间将成倍增长。

为了有效地发现数据库中某个key对应的value,我们需要不同的数据结构:索引。在这一章中,我们将研究一系列的索引结构,并对其进行比较; 它们背后的基本思想是将一些额外的元数据单独存储,充当路标,帮助你定位所需的数据。如果您想以几种不同的方式搜索相同的数据,您可能需要在数据的不同部分上使用几个不同的索引。

索引是派生自原始数据的附加结构。许多数据库允许您添加和删除索引,这并不影响数据库的内容;它只影响查询的性能。 维护额外的数据结构是有代价的,特别是在写操作上,对于写操作来说,无法在像之前那样简单将记录添加到文件中,任何类型的索引通常会减慢写操作,毕竟在写数据时索引也需要更新。

这在存储系统中是一个重要的权衡:选择良好的索引可以加快读取速度,但是每个索引都减慢了写入速度。 出于这个原因,数据库通常不会在默认情况下索引所有内容,但是需要你(应用程序开发人员或数据库管理员)结合你对应用程序主要查询模式的了解手动构建索引。 然后,你可以选择为你的应用程序构建最优的索引,而不必引入不必要的开销。

三、Hash Indexes

让我们从key-value数据的索引开始。这不是唯一可以作为索引实现的数据结构,但它非常常见,而且它是更复杂索引重要的组成部分。键值存储与大多数编程语言中可以找到的dictionary类型非常相似,通常是作为散列表(hashtable)实现的。hashtable在许多算法教科书中都有描述[1,2],因此我们不会详细讨论它们在这里是如何工作的。既然我们已经有了内存数据结构的哈希映射,为什么不使用它们来索引磁盘上的数据呢?

假设我们的数据存储只包括对文件的追加,就像前面的例子一样。 那么,最简单的索引策略是:保存一个内存哈希映射,其中每个key都映射到数据文件中的一个字节偏移位置,该位置存储有对应的数值,如图3-1所示。

无论何时将新的key-value对添加到文件中,您都会更新hashtable以反映您刚刚编写的数据的偏移量(这既适用于插入新键,也用于更新存在键)。当你想查找一个key时,使用hashtable来查找数据文件中的偏移量,定位该位置,并读取该值。

这听起来可能过于简单,但却是可行的方法。这实际上是(Bitcask 中的默认存储引擎)所做的[3]。

Bitcask 提供高性能的读和写,需要满足的前提就是所有的key都存储在内存里,因为hashtable被完全保留在内存中。 但是value并没有存在内存里,而是存储在磁盘上,所以Value可以使用比可用内存更多的空间。如果数据文件的那一部分已经在文件系统缓存中,那么读操作将不会有任何磁盘I/O。

像Bitcask这样的存储引擎非常适合于每个key的value经常更新的情况。 例如,key可能是一个视频的URL,而value可能是它被播放的次数(每次点击播放按钮时value都会增加)。 在这种工作负载中,会有很多写操作,但是key的数据量相对较少——每个key都有大量的写操作,但是在内存中保留所有key是可行的

到目前为止,我们一直对文件进行了追加,那么我们如何避免最终耗尽磁盘空间呢? 一个好的解决方案是,当它达到一定的大小时, 通过关闭这个文件,并将写入到一个新的文件中。这样每个文件中存储的都是一小段数据,称之为:Data file segment。然后我们可以对这些文件中的内容进行压缩,如图3-2所示。压缩意味着将文件中重复key合并,并且只保留每个key的最新值。

图3-2

此外,由于压缩常常使segment更小(假设一个key在一个segment内平均被重写了多次),我们也可以同时将所有的segment合并在一起执行压缩,如图3-3所示。segment 在写入后不会被修改,因此将合并之后的segment写入到一个新segment中。segment的合并和压缩可以在后台线程中完成,而在压缩合并进行的过程中,我们仍然可以使用旧的segment段文件,以正常的方式完成读和写。合并压缩过程完成之后,我们将读请求改为读取新合并的segment而不是旧的segment,然后旧的segment就可以被删除掉以释放磁盘空间。

图3-3

每个segment都有自己的内存hashtable作为索引。为了找到key对应的value的值,我们首先检查最近的segment的hashtable。如果key不存在,我们将检查第二个最近的部分,等等。segment合并的过程减少了segment的数量,所以查找操作不需要检查太多的hashtable。这个简单的想法在实践中有很多细节。简单地说,真正实现的一些重要问题是:

这种append-only log机制,第一眼看上去感觉会很浪费:为什么不更新文件,用新Value重写旧的Value?然而,append-only的设计是有优势的,原因如下:

然而Hash 索引也有它的局限性:

在下一节中,我们将会看到一个索引结构,它没有上面这些限制:

未完待续。。。。

微信公众号
上一篇下一篇

猜你喜欢

热点阅读