BTrees和Disk Persistance

时间:2016-07-28 17:49:26

标签: java file indexing b-tree randomaccessfile

有一段时间我正在为非常大的数据集(大约1.9亿)创建索引。我有一个可以插入数据集(通常是一个对象)/搜索密钥的BTree,当我搜索如何将数据保存到磁盘中的文件时,我遇到了这篇惊人的文章(http://www.javaworld.com/article/2076333/java-web-development/use-a-randomaccessfile-to-build-a-low-level-database.html#resources)。这几乎给了我起点。

这里他们将String键索引到二进制对象(blob)。它们具有文件格式,它们将它分为3个区域,标题(存储索引的起始点),索引(存储索引及其对应的位置)和数据区域(存储数据)。他们使用RandomAccessFile来获取数据。

如何为btree定义类似的文件格式。我所知道的是每次读取磁盘,我必须得到一个节点(通常一个块512字节)。关于如何坚持有许多类似的问题,但是很难理解为什么我们决定像我们这样的问题(Persisting B-Tree nodes to RandomAccessFile -[SOLVED])实施的事情的大局。请分享您的想法。

2 个答案:

答案 0 :(得分:1)

有大量的开源键/值存储和完整的数据库引擎 - 休息一周并启动谷歌搜索。即使你最终没有使用它们,你仍然需要研究一个代表性的横截面(架构,设计历史,关键实现细节),以获得关于主题的足够概述,以便您可以做出明智的决定并提出智能问题。有关简要概述,请尝试使用有关索引文件格式的Google详细信息,包括IDX或NTX等历史记录格式以及各种数据库引擎中使用的当前格式。

如果你想自己动手,那么你可以考虑搭配现有格式的潮流,比如dBASE变种Clipper和Visual FoxPro(我最喜欢的)。这使您能够使用现有工具处理数据,包括Total Commander插件和诸如此类的东西。您不需要支持完整格式,只需要为项目选择的格式的单个二进制实例。非常适合调试,重建索引,即席查询等。即使您不使用任何现有库,格式本身也很简单且易于生成。索引文件格式不是很简单,但仍然可以管理。

如果你想从头开始自己推动自己,那么你就有了一条前进的道路,因为在互联网和文献中,节点内(页内)设计和实践的基础知识很少。例如,一些旧的DDJ问题包含有关与前缀截断相关的有效密钥匹配的文章(又称'前缀压缩')等等,但我发现此时网络上没有任何可比的内容除了深深埋藏在一些研究论文或源代码库中之外。

这里最重要的一项是有效搜索前缀截断键的算法。一旦你得到了,其余的或多或少都会落到实处。我在网上找到了一个资源,就是这个DDJ(Dobb博士期刊)文章:

很多技巧也可以从像

这样的论文中收集

有关更多详细信息以及其他所有内容,您可以做的比阅读以下两本书涵盖(两者都有!)更糟糕:

后者的替代方案可能是

它涵盖了类似的频谱,它似乎更有动手,但它似乎没有相同的深度。我不能肯定地说(我已经订购了一份副本,但尚未获得它)。

这些书让你对所涉及的所有内容有一个完整的概述,它们实际上没有脂肪 - 即你需要知道那里的几乎所有东西。他们会回答你不了解的那些问题,或者你应该问自己的问题。它们涵盖了整个基础 - 从B树(和B +树)基础到详细的实现问题,如并发,锁定,页面替换策略等等。它们使您能够利用分散在网络上的信息,如文章,论文,实施说明和源代码。

话虽如此,我建议将节点大小与架构的RAM页面大小(4 KB或8 KB)相匹配,因为这样您就可以利用操作系统的分页基础设施而不是运行冲突它的。并且你可能最好将索引和blob数据保存在单独的文件中。否则,您无法将它们放在不同的卷上,并且数据会记录不属于您的程序(硬件,操作系统等)的子系统中的索引页面的缓存。

我绝对会选择B +树结构,而不是像普通B树那样使用数据来填充索引页面。我还建议使用间接向量(Graefe在那里有一些有趣的细节)与长度前缀键相关联。将密钥视为原始字节,并将所有整理/规范化/上下废话保留在核心引擎之外。用户可以根据需要为您提供UTF8 - 您不想关心它,相信我。

在内部节点中仅使用后缀截断(即用于区分John Smith' Lucky Luke' K' K'或者' L' L'工作与给定的密钥一样好)并且只在叶子中加上前缀截断(即代替John Smith'以及' John Smythe'你存储& #39; John Smith'和7 +')。

它简化了实施,并为您提供了大部分可以获得的爆炸效果。即共享前缀在叶级别(在索引顺序中的相邻记录之间)往往非常常见,但在内部节点中不是那么多,即在较高的索引级别。相反,叶片无论如何都需要存储完整的密钥,因此没有什么可以截断并扔掉那里,但内部节点只需要路由流量,你可以在页面中装入比非截断更多的截断键那些。

对一个充满前缀截断键的页面的键匹配非常有效 - 平均而言,每个键比较少于一个字符 - 但它仍然是线性扫描,即使所有的前进基于跳跃跳过计数。这有点限制了有效的页面大小,因为在截断的键面前二进制搜索更复杂。 Graefe有很多细节。实现更大节点大小(数千个密钥而不是数百个密钥)的一种解决方法是将节点布局为具有两个或三个级别的迷你B树。它可以使事情快速闪电(特别是如果你尊重像64字节缓存行大小这样的魔术阈值),但它也使代码变得非常复杂。

我采用简单精益和平均设计(范围与IDA's key/value商店相似),或使用现有产品/库,除非您正在寻找新的爱好...... < / p>

答案 1 :(得分:1)

根据在此期间已知的问题细节,这是对该问题的另一种选择。这篇文章基于以下假设:

  • 记录数约为1.9亿,已修复
  • 键是64字节的哈希值,如SHA-256
  • 值是文件名:可变长度,但是合理(平均长度<64字节,最大值<页面)
  • 页面大小4 KiByte

数据库中文件名的高效表示是一个不同的主题,这里无法解决。如果文件名很笨拙 - 平均来说很长和/或Unicode - 那么散列解决方案将会增加磁盘读取次数(更多溢出,更多链接)或降低平均占用率(浪费更多空间)。然而,B树解决方案反应更为温和,因为无论如何都可以构建最佳树。

在这种情况下最有效的解决方案 - 最简单的实现 - 是散列,因为你的密钥已经是完美的哈希。将哈希的前23位作为页码,并布置如下页面:

page header
    uint32_t next_page
    uint16_t key_count
key/offset vector
    uint16_t value_offset;
    byte key[64];

... unallocated space ...

last arrived filename
...
2nd arrived filename
1st arrived filename

值(文件名)从页面末尾向下存储,前缀为16位长度,键/偏移向量向上增长。这样,低/高键计数和短/长值都不会导致不必要的空间浪费,就像固定大小的结构一样。在密钥搜索期间,您也不必解析可变长度的结构。除此之外,我的目标是尽可能简单 - 没有过早的优化。堆的底部可以存储在页眉中,KO.[PH.key_count].value_offset(我的偏好),或计算为KO.Take(PH.key_count).Select(r => r.value_offset).Min(),无论你最喜欢什么。

键/偏移量向量需要在键上保持排序,以便您可以使用二进制搜索,但值可以在它们到达时写入,​​它们不需要按任何特定顺序。如果页面溢出,请在文件的当前末尾分配一个新文件(将文件增加一页)并将其页码存储在相应的标题槽中。这意味着您可以在页面内进行二进制搜索,但需要逐个读取和搜索所有链接的页面。此外,您不需要任何类型的文件头,因为文件大小是可用的,并且是唯一需要维护的全局管理信息。

将文件创建为稀疏文件,其页数由您选择的散列密钥位数表示(例如,23位的8388608页)。稀疏文件中的空页不占用任何磁盘空间并读取全0,这与我们的页面布局/语义完全一致。每当需要分配溢出页面时,将文件扩展一页。注意:&#39;稀疏文件&#39;这里的事情并不重要,因为几乎所有的页面都会在您构建文件时写入。

为了获得最高效率,您需要对数据进行一些分析。在我的模拟中 - 随机数作为哈希的替身,并且假设平均文件名大小为62字节或更少 - 最佳结果是制作2 ^ 23 = 8388608桶/页。这意味着您将哈希的前23位作为要加载的页码。以下是详细信息:

# bucket statistics for K = 23 and N = 190000000 ... 7336,5 ms

average occupancy 22,6 records
0 empty buckets (min: 3 records)
310101/8388608 buckets with 32+ records (3,7%)

这使链接保持在最低限度,平均每次搜索只需要阅读1.04页。将散列密钥大小增加一位到24会将预期的溢出页数减少到3,但会使文件大小翻倍,并将平均占用率降低到每页/桶的11.3条记录。将密钥减少到22位意味着几乎所有页面(98.4%)都可以溢出 - 这意味着文件的大小与23位大小相同,但每次搜索需要执行两倍的磁盘读取。

因此,您可以看到对数据进行详细分析以确定用于散列寻址的正确位数的重要性。您应该运行使用实际文件名大小的分析并跟踪每页开销,以查看22位到24位的实际图片。它需要一段时间才能运行,但这仍然比盲目地构建一个数GB的文件然后发现你浪费了70%的空间或搜索平均显着超过1.05页读取速度更快

任何基于B树的解决方案都会涉及更多(阅读:复杂),但由于显而易见的原因,无法将每次搜索的页面读取次数减少到1.000以下,甚至只能假设有足够数量的内部节点可以保存在内存中。如果您的系统具有如此大量的RAM,那么数据页面可以在很大程度上被缓存,那么散列解决方案将受益于基于某种B树的数据页面。

尽管我希望有一个借口来构建一个惊人的快速混合基数/ B +树,但散列解决方案可以为一小部分工作提供基本相同的性能。 B-treeish解决方案唯一可以超越散列的是空间效率,因为为现有的预先排序的数据构建最佳树是微不足道的。