【问题标题】:Multimaps in ChronicleMapChronicleMap 中的多图
【发布时间】:2016-07-28 18:30:10
【问题描述】:

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

编年史地图不是……

...没有二级索引。

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

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

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

  • 请按 ID 给我披萨。
  • 给我所有有特殊配料的比萨饼。
  • 把所有有特殊外皮的比萨都给我。

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

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

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

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

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

【问题讨论】:

    标签: java chronicle chronicle-map


    【解决方案1】:

    如果实际数据确实类似于比萨饼、配料和面包皮,i。 e.只有少数不同的浇头/外壳,并且成千上万的比萨饼包含它们中的每一个,我会说在这种情况下拥有适当的多图是过分的,你最好有pepperoni_pizzas.datonions_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 上运行应用程序,则可以通过为每个条目(通过在ChronicleMapBuilder 配置中指定.actualChunkSize())并依赖Linux 的惰性映射内存分配功能(根据需要逐页)。因此,对于每个 UUID 列表,您最多会丢失 4KB 的内存,如果列表有很多 KB 大小,这可能没问题。

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

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

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

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

    结论

    在某些用例中,通过适当的配置,Chronicle Map 可以用作多地图。在其他情况下,Chronicle Map 作为多图本身就效率低下。

    重要因素:

    • key 的总数 -> List&lt;Value&gt; multimap 中的条目
    • 值的总数
    • 密钥大小的平均值和分布
    • 不同值大小的平均值和分布
    • 值列表大小的平均值和分布
    • 值列表在 Chronicle Map 生命周期内的动态(从不更新、仅追加、删除和追加。从列表的开头和中间删除更昂贵。)
    • 编年史地图是否被复制

    【讨论】:

    • 这很有帮助,而且可能是我在 Stack Overflow 上得到的最好的答案。希望我能两次投票给你。非常感谢!
    • ListMarshaller 上的问题。如果我不使用 UUID,而只是使用整数(不太复杂),我还需要为整数创建读/写编组器吗?似乎已经有一个 IntegerMarshaller,所以我可能只做ListMarshaller.of(IntegerMarshaller.INSTANCE)
    • @Depressio Integer 类与 UUID 存在相同的问题 - 它是不可变的,因此 ListMarshaller 将需要在每次反序列化时创建大量对象。在整数的情况下,复制整个 ListMarshaller 类,并将其专门用于读取和写入原始整数而不是通用元素可能更容易和更有效。您还可以使用专门的原始容器(例如来自 fastutil 库)来存储堆上,而不是通用 ArrayList。
    • 好的,我试一试。不过,我对“可重用”类的概念有点困惑。由于 UUID 和 Integer 是不可变的,正如您所说,任何包含类似内容的类都不会在其中包含不可变对象,无论如何都需要大量对象进行反序列化?我假设“ReusableUUID”将是一个仅包含 UUID 的类,但反序列化包含的类是否也必须重新创建内部的、不可变的 UUID?似乎任何标识符都会归结为某种不可变的东西(int,String,等等)......
    • @Depressio 应该归结为原语——不是对象。 IE。 MutableUUID 应该有两个字段:long mostSigBits,long leastSigBits(与原始 UUID 相同),但这些字段可重新设置。如果应用一些重复数据删除技术,也可以保留Strings 原样,并且字符串内容通常相同。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-16
    • 1970-01-01
    相关资源
    最近更新 更多