Scala 中的 Disk-persisted-lazy-cacheable-List ™
Posted
技术标签:
【中文标题】Scala 中的 Disk-persisted-lazy-cacheable-List ™【英文标题】:Disk-persisted-lazy-cacheable-List ™ in Scala 【发布时间】:2012-02-21 21:17:00 【问题描述】:我需要在 Scala 中有一个非常非常长的对 (X, Y) 列表。太大了,它不适合内存(但很适合放在磁盘上)。
所有更新操作都是缺点(头部附加)。 所有读取访问都从头部开始,依次遍历列表,直到找到预先确定的对。 缓存会很棒,因为大多数读取访问会一遍又一遍地保留相同的数据。所以,这基本上是一个“disk-persisted-lazy-cacheable-List”™
在我开始推出自己的产品之前,有什么想法吗?
附录: 是的.. mongodb 或任何其他不可嵌入的资源都是过大的。如果您对此特定用例感兴趣,请参阅课程 Timeline
here。基本上,我有一个非常非常大的时间表(几个月内有数百万对),尽管我的比赛只需要触及最后几个小时。
【问题讨论】:
如果你最终自己滚动,你可能想要实现一些基于页面的东西。 head-appends 的要求让事情变得有趣,因为文件是可附加的,但只能在最后,并且大概你不想通读整个文件来读取最新的值。 所以要明确一点,您正在寻找基于 Scala 的解决方案,而不是基于操作系统的解决方案?处理磁盘和内存之间的分页和交换通常被视为操作系统服务。 “所有读取访问都从头部开始”和“非常非常长的对列表”...您确定要 O(n) 查找吗? @Chris Shain:文件应该以相反的顺序存储列表,所以添加到列表之前就是添加到文件中 我不想随机访问集合,所以我希望头部为 O(1),前 n 个元素为 O(n)。 【参考方案1】:进行此类操作的最简单方法是扩展Traversable
。您只需要定义foreach
,并且您可以完全控制遍历,因此您可以执行诸如打开和关闭文件之类的操作。
您还可以扩展Iterable
,这需要定义iterator
,当然,还需要返回某种Iterator
。在这种情况下,您可能会为磁盘数据创建一个Iterator
,但是要控制诸如打开文件之类的东西会变得更加困难。
这是我所描述的Traversable
的一个示例,由 Josh Suereth 编写:
class FileLinesTraversable(file: java.io.File) extends Traversable[String]
override def foreach[U](f: String => U): Unit =
val in = new java.io.BufferedReader(new java.io.FileReader(file))
try
def loop(): Unit = in.readLine match
case null => ()
case line => f(line); loop()
loop()
finally
in.close()
【讨论】:
有趣,但这只会解决阅读部分(并且没有任何类型的缓存)。我一直在寻找关于cons
操作和缓存管理的透明内容。
@HugoSFerreira 这不是您问题的解决方案,它只是一个示例,说明如何扩展 Traversable
以处理内存外集合。【参考方案2】:
此 Java 库可能包含您需要的内容。它旨在比标准 Java 集合更有效地将条目存储在内存中。
http://code.google.com/p/vanilla-java/wiki/HugeCollections
【讨论】:
快进5年,这个项目现在在github.com/OpenHFT/Chronicle-Queue和github.com/OpenHFT/Chronicle-Map开发 感谢您添加评论。有趣的是,我当前的项目使用 ChronicleMap 存储 MD5 数据总和以检测重复!【参考方案3】:你写:
mongodb,或者任何其他不可嵌入的资源,都是一种过度杀伤
您知道有嵌入式数据库引擎,包括一些非常小的引擎吗?如果您知道,我不确定您的确切要求以及您为什么不使用它们。
你确定 Hibernate + embeddable DB(比如 SQLite)还不够吗? 或者,BerkeleyDB Java Edition、HSQLDB 或 other embedded databases 可能是一个选项。
如果您不对对象本身执行查询(听起来您确实没有),也许序列化会比复杂对象的对象关系映射更简单,但我从未尝试过,我也没有知道哪个会更快。但是序列化可能是在类型中完全通用的唯一方法,假设您选择的框架提供了一个合适的接口来编写[T <: Serializable]
。如果没有,您可以在创建自己的“类型类”MySerializable[T]
(例如 Scala 标准库中的 Ordering[T]
)之后编写 [T: MySerializable]
。
但是,您不想为此任务使用标准 Java 序列化。 “任何可序列化的东西”听起来是一个糟糕的要求,因为它建议为此使用序列化,但我想你可以将其放松为“使用我选择的框架可序列化的任何东西”。序列化在时间和空间上效率极低,并且不是为序列化单个对象而设计的,而是为您返回一个带有特殊标题的文件。我建议使用一些不同的序列化框架 - 看看here 进行比较。
不走自定义实现之路的其他原因
此外,听起来您实际上是在向后读取文件,从性能角度来看,在非 SSD 磁盘上,这是一种非常糟糕的访问模式:读取一个扇区后,几乎需要完整的磁盘轮换才能访问上一个。
此外,正如 Chris Shain 在上面的评论中指出的那样,您需要使用基于页面的解决方案,并且您需要处理可变大小的对象。
【讨论】:
【参考方案4】:如果您不想升级到其中一个可嵌入数据库,那么memory mapped files 中的堆栈怎么样?
堆栈似乎满足您所需的访问特性。 (推送一堆数据,频繁迭代最近推送的数据) 您可以直接从 Scala 使用 Java 的 MappedByteBuffer。您可以像处理内存一样对文件进行寻址,而无需尝试将文件实际加载到内存中。 您可以通过这种方式从操作系统免费获得一些缓存,因为映射文件的功能类似于虚拟内存。最近写入/访问的页面将保留在操作系统文件缓存中,直到操作系统认为适合将它们刷新(或您手动刷新它们)回磁盘 如果您担心顺序读取性能,您可以从文件的任一端构建堆栈,但如果您通常读取刚写入的数据,我认为这不会是一个问题,因为它仍然会是在记忆中。 (不过,如果您正在跨页面读取数小时/数天写入的数据,那么这可能是个问题) 即使在 64 位 JVM 上,以这种方式寻址的文件的大小也被限制为 2GB,但您可以使用多个文件来克服此限制。【讨论】:
缓存点很好,我也想过,但是我没有弄清楚如何将对象数组高效地转换为字节数组。你不能轻易地使用内存映射文件作为你的堆,不像在 C 中(它也不是那么简单,因为你仍然需要实现一个内存分配器)。以上是关于Scala 中的 Disk-persisted-lazy-cacheable-List ™的主要内容,如果未能解决你的问题,请参考以下文章