ChronicleMap 中的多重地图

Multimaps in ChronicleMap

关于 ChronicleMap 中的多地图,ChronicleMap's GitHub 上肯定有免责声明:

Chronicle Map is not...

... No secondary indexes.

A multimap. Using a ChronicleMap<K, Collection<V>> as multimap is technically possible, but often leads to problems...

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

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

我可以使用 ChronicleMap<UUID,Pizza> 轻松存放比萨饼。但这只是一种访问模式。我不想遍历每个比萨饼来找到配料或外皮相匹配的比萨饼。所以,我想存储 ChronicleMap<Topping,Collection<UUID>>ChronicleMap<Crust,Collection<UUID>>.

之类的东西

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

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

针对潜在问题的一些补充说明:

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

如果实际数据确实类似于比萨饼、浇头和面包皮,我。 e.只有少数几个不同的 toppings/crusts,而成千上万的比萨包含它们中的每一个,我想说对于这种情况,拥有适当的多图是过大的,你最好有 pepperoni_pizzas.datonions_pizzas.dat, ... 具有 UUID 的不同可附加共享列表,您可以使用 Chronicle Queue 方便地从多个进程访问和更新它们。

如果有成千上万个 toppings/crusts,平均只有 10-100 个比萨有特定的配料,您确实应该使用 multimap。

Chronicle-Maps-as-multimaps 基本上有 3 种 "problems":

每个查询的垃圾分配过多

如果您使用 List<UUID>Set<UUID> 类型的值创建 Chronicle Map 而不指定自定义值序列化程序,它可以工作,但效率会非常低,因为它将默认为内置-在 Java 序列化中,用于对每个请求的整个值集合进行序列化和反序列化,既不重用集合堆对象,也不重用元素的单个 UUID 堆对象。因此,每次对 ChronicleMap 的请求都会产生大量垃圾。

解决方案 但是,如果您将值序列化器指定为 ListMarshaller or SetMarshaller(或您的自定义集合编组器,您可以根据 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 附加到 off-堆内存块。如果你使用复制,它会将 100 个 UUID 的整个列表作为更新值发送到其他节点,而不是只发送一个添加的 UUID。

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

Chronicle-Map 内存碎片化

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

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

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

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

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

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

结论

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

重要因素:

  • 键总数 -> List<Value> 多图中的条目
  • 值总数
  • 密钥大小的平均值和分布
  • 不同值大小的平均值和分布
  • 值列表大小的平均值和分布
  • Chronicle Map 生命周期内的值列表动态(从不更新,仅附加,删除和附加。从列表的开头和中间删除更昂贵。)
  • 是否复制编年史地图