ChronicleMap中的多重映射

时间:2016-04-07 17:45:43

标签: java chronicle chronicle-map

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

  

Chronicle Map不是......

     

......没有二级索引。

     

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

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

让我试着用披萨来解释我的问题。我有10万种不同的比萨饼。每个披萨都有一个ID和许多不同的浇头和面包皮。我有三种访问模式:

  • 通过身份证给我披萨。
  • 给我所有有特别馅料的比萨饼。
  • 给我所有有特殊外壳的比萨饼。

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

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

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

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

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

1 个答案:

答案 0 :(得分:18)

如果实际数据确实类似于比萨饼,浇头和面包皮,我。即只有少数不同的浇头/外壳,而且成千上万的比萨饼都含有它们,我会说拥有一个合适的多图谱对于这种情况来说是过度的,你最好有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。

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

Chronicle-Map的内存碎片

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

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

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

内存过量使用并依赖于Linux中的延迟映射内存分配的技巧也适用于值的短列表(集合),但仅限于元素本身很大,因此平均总值大小为多KB。

当您可以以任何其他方式避免进入内存重新分配时,碎片也不是问题,i。即新的比萨UUID会及时添加,但也会被删除,因此顶级到美元的列表大小会在一些平均值附近浮动,重新分配很少会被删除。

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

结论

在某些用例中,如果配置正确,Chronicle Map可以充当多图。在其他情况下,Chronicle Map as multimap本质上是低效的。

重要因素:

  • 密钥总数 - &gt;多地图中的List<Value>条目
  • 值总数
  • 密钥大小的平均值和分布
  • 不同值大小的平均值和分布
  • 值列表大小的平均值和分布
  • 值列出Chronicle Map生命周期的动态(从不更新,仅追加,删除和追加。从列表的开头和中间删除更昂贵。)
  • 如果复制了Chronicle Map,那么