在 Java 中使用并发在磁盘上创建键值存储

Posted

技术标签:

【中文标题】在 Java 中使用并发在磁盘上创建键值存储【英文标题】:Creating a Key-Value Store on Disc with Concurrency in Java 【发布时间】:2011-10-19 08:43:33 【问题描述】:

我需要读取一组文件并将其分解为键值对,并将它们保存为磁盘上该键的(键、值列表),就像 map-reduce 范例一样。一切都在一台计算机上。例如,我可以在不同的文件上编写不同的列表并用密钥命名文件。这似乎是一种非常糟糕的做事方式。首先,如果你有十亿个密钥,你最终会得到十亿个文件。所以很明显那是行不通的,我需要某种内存映射。我还必须让不同的线程来执行映射工作,所以如果它们要写入同一个缓冲区,它们之间就必须进行某种同步。如果我有一个键值缓冲区映射,并在缓冲区上同步,那么线程不应该互相踩踏,所以我认为这部分应该可以工作。问题是如何将值映射到光盘。如何在同一个文件中写入对应于不同键的缓冲区?如果有人能指出我正确的方向,将不胜感激。我对这方面的了解非常可悲。再次感谢。

【问题讨论】:

您实际上想要做什么?应用程序是什么?你谈论缓冲和并发,但并没有真正给出一个理由来解决这个问题(这使得你很难理解你为什么要提出这些问题。)你真的有十亿个键吗?还是一百万?这是您尝试做的实际要求吗? 我不能说应用程序到底是什么,但它与在单机上编写 MapReduce 非常相似。从文件中读取输入,将其分解为键值对,然后收集特定键的值。所有步骤都必须在磁盘上完成,因为数据包含在数十亿个密钥中。数十亿,而不是数百万。 @delmet,我不明白你为什么需要“键值缓冲区映射”?值已经映射到键...如何映射缓冲区?无论如何,请参阅我的答案以获取更多详细信息。 【参考方案1】:

Chronicle Map应该是解决这个问题的好方法。

通常它在操作速度和消耗的内存方面都非常有效,即。 e.它是 much faster,而不是之前建议的 BerkeleyDB。

Chronicle Map 是一种分段存储,允许并行处理分段,例如。克:

for (int i = 0; i < chronicleMap.segments(); i++) 
  int segmentIndex = i;
  executor.submit(() -> 
    chronicleMap.segmentContext(segmentIndex).forEachSegmentEntry(entry -> 
      // do processing with entry.key() and entry.value(),
      // value() could be a List or some Iterator-like abstraction
    );
  );

MapSegmentContext Javadocs。

但是,每个键 could not always be handled efficiently with Chronicle Map 具有(逻辑上)多个值。但是在您的情况下,如果您只需要处理每个键的静态值集,而不是添加/删除值,它可能会很好。

【讨论】:

【参考方案2】:

你看过使用Hadoop吗?

【讨论】:

【参考方案3】:

从实际的角度来看,使用 BerkeleyDB 很容易做到这一点,as Lirik suggested.

如果您对理论比对实践更感兴趣,我建议您将其视为“外部排序”操作。也就是说,将尽可能多的输入读入内存,然后按键排序。将已排序的块写为单个文件。然后可以轻松地将排序后的文件合并到一个文件中。

在其他应用程序中,这是 Lucene 用来构建“倒排索引”以搜索文本的方法。 “键”是文档中的单词,“值”是出现该单词的文档列表。 Lucene 读取文档,并为每个单词在内存中创建一个术语到文档的条目。当内存已满时,它将索引段写入磁盘。当磁盘上有很多索引段时,将它们合并为一个段。事实上,您也可以根据您的任务调整 Lucene 的索引编写器。

可以将工作划分为多个线程。但是,您必须对磁盘争用敏感。跳过同时读取和写入许多文件会大大降低传统驱动器的速度。可能有机会同时安排一些活动。当您将前一个已排序的块写入磁盘时,您可能会从一个文件中读取新数据,尤其是在机器有两个磁盘驱动器的情况下。当然,使用 SSD 临时存储一些已排序的段会很有帮助。

【讨论】:

当他说“如何实现磁盘上的键值映射”时,这就是他要问的吗?无论如何,是的,这正是你要做的!另一件要提的是,一旦你进行了外部排序,你就可以用段的内容更新数据库并摆脱段。 是的,这就是我所说的。对不起,不够清楚。这也是我相信在 Hadoop 中完成事情的方式,以及为什么密钥需要具有可比性。我想知道天气只能一步完成,但我认为这是不可能的。您需要排序和合并。【参考方案4】:

我认为Oracle's Berkeley DB 可能正适合你:

Berkeley DB 旨在将数据存储为键/值对中数据的不透明字节数组,这些数据以可用访问方法之一为索引,如上所示。

Berkeley 非常健壮、成熟且快速,但如果您想采用更轻量级的方法,请使用SQLite。

另一种选择是使用 Google 的 LevelDB;它是用 C++ 编写的,但有 Java wrappers around it。 LevelDB 速度快得令人麻木,而且非常轻量级!

没有更多关于你的项目的细节,我只能说:

使用所有这些解决方案,键/值对将存储在同一个文件中(如有必要,多个实例可以存储到单独的文件中,但我不明白为什么会这样)。 BerkeleyDB 和 LevelDB 具有非常好的缓存和映射功能。 BDB 和 LDB 也允许压缩(不确定 SQLite 是否也支持)。 根据您的密钥分布(即,如果您使用像 Google 的 CityHash 这样的良好哈希函数),您可能会获得非常好的数据局部性,从而减少表扫描。 您可能应该编写自己的线程安全缓冲区,并且应该避免让多个线程写入 BDB/LDB,因为这些解决方案是基于磁盘的,并且您通常不希望进行多线程磁盘 I/O 操作。

评论: - 我不确定您所说的“键值缓冲区映射”是什么意思......您是否将缓冲区映射到每个键?为什么需要它?

【讨论】:

这肯定行得通,但如果可以完成磁盘上的键值映射,那就太好了。我想这不是一个小问题。 @delmet 我不确定我是否理解您在这里的要求:值作为数据库底层结构的一部分映射到键,并且数据库存储在磁盘上(不在记忆)。插入、更新、读取等都是在给定的键上完成的。磁盘上的键值映射是在将值映射到键时完成的。这有意义吗? 我说的是上面的 Ericson 说的是什么。在那里看到我的评论。感谢您的帮助。

以上是关于在 Java 中使用并发在磁盘上创建键值存储的主要内容,如果未能解决你的问题,请参考以下文章

android上持久键值存储的最佳机制是啥(具有大值)

分布式、一致的键值存储系统在处理并发请求时如何返回最新键的基础知识?

什么是hashMap,初始长度,高并发死锁,java8 hashMap做的性能提升

java问题,我想在java中存储键值对,以便使用,但是键值对的键和值都有重复元素,使用hashmap会产生覆盖。

为高级查询选择基于 Java 的键值存储

数据存储方式——KVELL:快速持续键值存储的设计与实现