编年史地图中的多地图

2022-09-03 05:37:13

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

编年史地图不是...

...无二级索引。

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

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

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

  • 按身份证给我比萨饼。
  • 给我所有有特殊浇头的比萨饼。
  • 给我所有有特定外壳的比萨饼。

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

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

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

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

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

答案 1

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

如果有10-100个成千上万的浇头/外壳,只有10-100个比萨饼平均有特定的浇头,你应该使用多地图。

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

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

如果在未指定自定义值序列化程序的情况下使用或类型创建具有值的 Chronicle Map,则该映射将起作用,但它将完全低效,因为它将默认为内置的 Java 序列化,以便在每个请求上序列化和反序列化整个值集合,而无需重用集合堆对象,也不重用元素的单个堆对象。因此,在对ChronicleMap的每个请求中都会生成大量垃圾。List<UUID>Set<UUID>UUID

溶液但是,如果将值序列化程序指定为 ListMarshallerSetMarshaller(或您的自定义集合元组,您可以基于它编写和实现),并将其与可重用的 UUID 堆对象结合使用,它将解决以下垃圾问题:ListMarshallerSetMarshaller

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。

两者(价值更新和复制)都可以通过可怕的黑客来优化,但它需要对Chronicle Map的实现有非常深入的了解,并且将非常脆弱。

编年史地图记忆的碎片化

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

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

另一方面,如果你的列表很长(它们是UUID的列表,即小结构),而你总共只有100 000个比萨饼,那么你首先不需要多地图,请参阅这个答案的开头。

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

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

如果在条目插入到编年史地图后,值从未更新(或大小从不更改),则内存碎片永远不会成为问题。

结论

在某些用例中,通过适当的配置,Chronicle Map可以作为多映射井。在其他情况下,作为多地图的编年史地图本质上是低效的。

重要因素:

  • 多映射中键 ->条目的总数List<Value>
  • 值的总数
  • 密钥大小的平均值和分布
  • 不同值大小的平均值和分布
  • 值列表大小的平均值和分布
  • 值列出编年史地图生存期内的动态(从不更新、仅追加、移除和追加。从列表的开头和中间删除的成本更高。
  • 是否复制了编年史地图

答案 2

推荐