15

ChronicleMap 的 GitHub 上肯定有关于ChronicleMap 中的Multimaps 的免责声明:

纪事地图不是...

...没有二级索引。

多图。使用ChronicleMap<K, Collection<V>>as multimap 在技术上是可行的,但通常会导致问题......

不幸的是,这是我的用例之一,为此使用堆外存储(使用 ChronicleMap)肯定是最简单的方法。

让我试着解释一下我对披萨的问题。我有 100,000 种不同的比萨饼。每个比萨饼都有一个 ID 和许多不同的配料和外壳。我有三种访问模式:

  • 按 ID 给我披萨。
  • 给我所有有特殊配料的比萨饼。
  • 给我所有有特殊外壳的比萨饼。

我可以使用ChronicleMap<UUID,Pizza>. 但这只是一种访问模式。我不想遍历每个比萨饼来找到具有匹配顶部或外壳的比萨饼。所以,我想存储类似ChronicleMap<Topping,Collection<UUID>>and的东西ChronicleMap<Crust,Collection<UUID>>

然后,如果有人问我所有的意大利辣香肠披萨,我会在顶部的 ChronicleMap 中查找匹配披萨的 UUID,然后在主披萨地图中查找。

但是上面引用的文档让我害怕。有谁知道这样的事情经常导致的这些“问题”可能是什么?为什么我不应该这样做,即使它似乎对我有用?它与 ChronicleMap 如何存储序列化对象,特别是集合有关吗?

针对潜在问题的一些附加说明:

  1. 我们稍后可能会添加需要更新集合的比萨饼。
  2. 许多进程都在尝试执行这些操作,因此需要通过 ChronicleMap 共享地图,而不仅仅是基本的 ConcurrentMap。
4

1 回答 1

25

如果实际数据确实类似于比萨饼、浇头和外壳,即只有少数不同的浇头/外壳,并且成千上万的比萨饼包含它们中的每一个,我会说对于这种情况来说,拥有一个适当的多图是矫枉过正的,你最好有pepperoni_pizzas.dat, onions_pizzas.dat, ... 具有 UUID 的不同可附加共享列表,您可以使用Chronicle Queue方便地从多个进程访问和更新它们。

如果有 10s-100s 的数千个浇头/外壳,平均只有 10s-100s 的比萨饼有特定的浇头,那么您确实应该使用 multimap。

从本质上讲,Chronicle-Maps-as-multimaps 存在 3 种“问题”:

每个查询上的过多垃圾分配

如果您在没有指定自定义值序列化器的情况下创建具有值List<UUID>Set<UUID>值类型的 Chronicle Map,它会起作用,但效率会非常低,因为它将默认使用内置 Java 序列化来序列化和反序列化每个请求的整个值集合,既不重用集合堆对象,也不重用UUID元素的单个堆对象。因此,对 ChronicleMap 的每个请求都会产生大量垃圾。

解决方案 但是,如果您将值序列化程序指定为ListMarshallerSetMarshaller(或您可以基于ListMarshallerSetMarshaller实现编写的自定义集合编组器)与可重用的 UUID 堆对象一起指定,它将解决这个垃圾问题:

ListMarshaller<ReusableUuid> valueMarshaller = ListMarshaller.of(
     ReusableUuidReader.INSTANCE, ReusableUuidWriter.INSTANCE);
List<ReusableUuid> averageValue = Stream
    .generate(() -> ReusableUuid.random())
    .limit(averagePizzasForTopping)
    .collect(Collectors.toList());
 ChronicleMap<Topping, List<ReusableUuid>> map = ChronicleMap
     .of(Topping.class, (Class<List<ReusableUuid>>) (Class) List.class)
     .averageKey(pepperoni)
     .valueMarshaller(valueMarshaller)
     .averageValue(averageValue)
     .entries(numberOfToppings)
     .createPersistedTo(new File("toppings_to_pizza_ids.dat"));

低效的价值更新和复制

当您将另一个披萨 UUID 附加到 100 个 UUID 的列表中,并将新值插入 Chronicle Map 时,Chronicle Map 将再次重写整个列表,而不是将一个 UUID 附加到堆外内存块的末尾。如果你使用复制,它会将 100 个 UUID 的整个列表作为更新值发送到其他节点,而不是只发送一个添加的 UUID。

两者(值更新和复制)都可以通过可怕的 hack 进行优化,但它需要非常深入的 Chronicle Map 实现知识,并且非常脆弱。

Chronicle-Map的记忆碎片

如果您计划在数据存储生命周期内添加新的比萨饼,最初为整体分配的内存区域将变得太小而无法容纳具有更多 UUID 的新值,因此将重新分配内存区域(可能为每个 UUID 列表分配多次)。Chronicle Map 的数据结构设计意味着简化的内存分配方案,如果条目被多次重新分配,它会严重地受到碎片的影响。

如果列表中有很多 UUID,并且在 Linux 上运行应用程序,则可以通过为每个条目(通过.actualChunkSize()ChronicleMapBuilder配置中指定) 并依赖于 Linux 的惰性映射内存分配特性(根据需要逐页)。因此,对于每个 UUID 列表,您最多会丢失 4KB 的内存,如果列表有很多 KB 大小,这可能没问题。

另一方面,如果您的列表很长(并且它们是 UUID 列表,即小型结构),并且您总共只有 100 000 个比萨饼,那么您首先不需要 multimap,请参阅此答案的开头.

Linux 中内存过度使用和依赖惰性映射内存分配的技巧也适用于值的短列表(集合),但前提是元素本身很大,因此平均总值大小为许多 KB。

当您可以通过任何其他方式避免条目内存重新分配时,碎片也不是问题,即新的比萨 UUID 会及时添加但也会被删除,因此 topping-to-uuids 列表大小浮动在某个平均值附近并且很少重新分配击中。

如果在将条目插入 Chronicle Map 后值从未更新(或大小从未改变),则内存碎片永远不会成为问题。

结论

在某些用例和适当的配置下,Chronicle Map 可以用作多地图。在其他情况下,Chronicle Map 作为多图本质上是低效的。

重要的因素:

  • 多图中的键总数 ->List<Value>条目
  • 值的总数
  • 密钥大小的平均值和分布
  • 不同值大小的平均值和分布
  • 价值列表大小的平均值和分布
  • 值列表在 Chronicle Map 生命周期内的动态(从不更新、仅追加、删除和追加。从列表的开头和中间删除更昂贵。)
  • 如果编年史地图被复制,或者没有
于 2016-04-07T20:27:28.920 回答