Hazelcast无法与SqlPredicate和可选字段上的索引一起正常使用

时间:2019-01-08 16:21:46

标签: java hazelcast

我们将复杂对象存储在Hazelcast映射中,不仅需要根据关键字而且还可以根据这些复杂对象的内容来搜索对象。为了避免对性能造成太大影响,我们在这些搜索字词上使用了索引。

我们还使用spring-data-hazelcast,它提供的存储库使我们可以使用findByAbcXyz()类型的语义查询。对于某些更复杂的查询,我们使用@Query批注(spring-data-hazelcast在内部将其转换为SqlPredicates)。

我们现在遇到了一个问题,即在某些情况下,即使我们可以验证搜索对象确实存在于地图中,这些基于@Query的搜索方法也不返回任何值。

我设法通过核心hazelcast(即不使用spring-data-hazelcast)重现此问题。

这是我们的对象结构:

BetriebspunktKey.java

public class BetriebspunktKey implements Serializable {
  private Integer uicLand;
  private Integer nummer;

  public BetriebspunktKey(final Integer uicLand, final Integer nummer) {
    this.uicLand = uicLand;
    this.nummer = nummer;
  }

  public Integer getUicLand() {
    return uicLand;
  }

  public Integer getNummer() {
    return nummer;
  }
}

Betriebspunkt.java

public class Betriebspunkt implements Serializable {
  private BetriebspunktKey key;
  private List<BetriebspunktVersion> versionen;

  public Betriebspunkt(final BetriebspunktKey key, final List<BetriebspunktVersion> versionen) {
    this.key = key;
    this.versionen = versionen;
  }

  public BetriebspunktKey getKey() {
    return key;
  }
}

BetriebspunktVersion.java

public class BetriebspunktVersion implements Serializable {
  private List<BetriebspunktKey> zusatzbetriebspunkte;

  public BetriebspunktVersion(final List<BetriebspunktKey> zusatzbetriebspunkte) {
    this.zusatzbetriebspunkte = zusatzbetriebspunkte;
  }
}

在我的主文件中,我现在正在设置hazelcast:

Config config = new Config();
final MapConfig mapConfig = config.getMapConfig("points");
mapConfig.addMapIndexConfig(new MapIndexConfig("versionen[any].zusatzbetriebspunkte[any].nummer", false));

HazelcastInstance instance = Hazelcast.newHazelcastInstance(config);

IMap<BetriebspunktKey, Betriebspunkt> map = instance.getMap("points");

我还在为以后准备搜索条件:

Predicate equalPredicate = Predicates.equal("versionen[any].zusatzbetriebspunkte[any].nummer", 53090);
Predicate sqlPredicate = new SqlPredicate("versionen[any].zusatzbetriebspunkte[any].nummer=53090");

接下来,我要创建两个对象,一个对象具有“完整深度”的信息,另一个对象不包含任何“ zusatzbetriebspunkte”:

final Betriebspunkt abc = new Betriebspunkt(
        new BetriebspunktKey(80, 166),
        Collections.singletonList(new BetriebspunktVersion(
            Collections.singletonList(new BetriebspunktKey(80, 53090))
        ))
    );

    final Betriebspunkt def = new Betriebspunkt(
        new BetriebspunktKey(83, 141),
        Collections.singletonList(new BetriebspunktVersion(
            Collections.emptyList()
        ))
    );

在这里,事情变得有趣起来。如果我先将“完整”对象插入到地图中,则使用EqualPredicate和SqlPredicate的搜索都将起作用:

map.put(abc.getKey(), abc);
map.put(def.getKey(), def);

Collection<Betriebspunkt> equalResults = map.values(equalPredicate);
Collection<Betriebspunkt> sqlResults = map.values(sqlPredicate);

assertEquals(1, equalResults.size()); // contains "abc"
assertEquals(1, sqlResults.size());   // contains "abc"

但是,如果我以相反的顺序将对象插入到我的地图中(即,首先是“部分”对象,然后是“完整”对象),则只有EqualPredicate可以正常工作,无论该对象是什么,SqlPredicate都会返回一个空列表。地图内容或搜索条件。

map.put(abc.getKey(), abc);
map.put(def.getKey(), def);

Collection<Betriebspunkt> equalResults = map.values(equalPredicate);
Collection<Betriebspunkt> sqlResults = map.values(sqlPredicate);

assertEquals(1, equalResults.size()); // contains "abc"
assertEquals(1, sqlResults.size());   // --> this fails, it returns en empty list

此行为的原因是什么?看起来好像是hazelcast代码中的错误。

1 个答案:

答案 0 :(得分:1)

失败的原因

经过大量调试,我找到了此问题的原因。确实可以在hazelcast代码中找到原因。

将值放入榛树映射时,将调用DefaultRecordStore.putInternal。在此方法结束时,将调用DefaultRecordStore.saveIndex,该方法将找到相应的索引,然后调用Indexes.saveEntryIndex。该方法遍历每个索引并调用InternalIndex.saveEntryIndex(或更确切地说,其实现IndexImpl.saveEntryIndex。该方法的有趣之处在于以下几行:

if (this.converter == null || this.converter == TypeConverters.NULL_CONVERTER) {
      this.converter = entry.getConverter(this.attributeName);
}

显然,当第一个元素放入映射中时,每个索引都存储一个转换器类。看着QueryableEntry.getConverter会说明发生了什么:

  TypeConverter getConverter(String attributeName) {
    Object attribute = this.getAttributeValue(attributeName);
    if (attribute == null) {
      return TypeConverters.NULL_CONVERTER;
    } else {
      AttributeType attributeType = this.extractAttributeType(attributeName, attribute);
      return attributeType == null ? TypeConverters.IDENTITY_CONVERTER : attributeType.getConverter();
    }
  }

第一次插入“完整”对象时,extractAttributeType()将遵循我们的索引定义“ versionen [any] .zusatzbetriebspunkte [any] .nummer”的“路径”,并发现nummer是整数类型,因此将返回并存储TypeConverters.IntegerConverter。

首次插入“部分”对象时,“ zusatzbetriebspunkte [any]”为空,extractAttributeType无法找出nummer具有的类型,因此它返回null,这意味着使用TypeConverters.IdentityConverter。

此外,每当插入“完整”元素时,都使用nummer作为键将条目写入索引映射,即索引映射的类型为Map。

要写地图太多了。现在让我们看看如何从地图读取数据。呼叫map.values(predicate)时,我们最终会到达QueryRunner.runUsingGlobalIndexSafely,其中包含一行:

Collection<QueryableEntry> entries = indexes.query(predicate);

这将在调用一些样板代码之后依次出现

Set<QueryableEntry> result = indexAwarePredicate.filter(queryContext);

对于我们的两个谓词,我们最终都将进入IndexImpl.getRecords(),其外观如下:

  public Set<QueryableEntry> getRecords(Comparable attributeValue) {
    long timestamp = this.stats.makeTimestamp();
    if (this.converter == null) {
      this.stats.onIndexHit(timestamp, 0L);
      return new SingleResultSet((Map)null);
    } else {
      Set<QueryableEntry> result = this.indexStore.getRecords(this.convert(attributeValue));
      this.stats.onIndexHit(timestamp, (long)result.size());
      return result;
    }
  }

关键的调用是this.convert(attributeValue),其中attributeValue是谓词的value

如果我们比较两个谓词,我们可以看到EqualPredicate有两个成员:

attributeName = "versionen[any].zusatzbetriebspunkte[any].nummer"
value = {Integer} 53090

SqlPredicate包含初始字符串(我们将其传递给其构造函数),但该字符串在构造时也被解析并映射到内部的EqualPredicate(在评估谓词时最终使用该字符串并将其传递给上述的getRecords()):

sql = "versionen[any].zusatzbetriebspunkte[any].nummer=53090"
predicate = {EqualPredicate}
  attributeName = "versionen[any].zusatzbetriebspunkte[any].nummer"
  value = {String} "53090"

这说明了为什么手动创建的EqualPredicate在两种情况下都起作用:它的值是整数。传递给转换器时,无论是IntegerConverter还是IdentityConverter都没有关系,因为两者都将返回整数,然后该整数可用作索引映射中的键(使用整数作为键)。

但是,对于SqlPredicate,该值为字符串。如果将其传递给IntegerConverter,则会将其转换为其对应的整数值,并且可以访问索引映射。如果将其传递给IdentityConverter,则转换返回该字符串,并且尝试使用字符串访问索引映射将永远找不到任何结果。

可能的解决方案

我们如何解决这个问题?我看到了几种可能性:

  • 在启动过程中将一个“完全构建的”虚拟值插入到我们的映射中,以确保正确初始化了转换器。虽然有效,但它很丑陋,而且不便于维护
  • 避免使用SqlPredicate,而使用基于整数的EqualPredicate。使用spring-data-hazelcast时,这不是一个选项,因为它总是将基于@Query的搜索转换为SqlPredicates。当然,我们可以直接使用hazelcast并绕过spring-data包装器,但这虽然可行,但这意味着有两种访问hazelcast的方式,这种方式也不易维护
  • 使用hazelcast的ValueExtractor类。这是一种优雅的解决方案,既可以在本地使用,也可以使用spring-data-hazelcast进行工作。我将概述一下外观:

首先,我们需要实现一个值提取器,以适合我们的形式返回Betriebspunkt的所有zusatzbetriebspunkte

public class BetriebspunktExtractor extends ValueExtractor<Betriebspunkt, String> implements Serializable {
  @Override
  public void extract(final Betriebspunkt betriebspunkt, final String argument, final ValueCollector valueCollector) {
    betriebspunkt.getVersionen().stream()
                 .map(BetriebspunktVersion::getZusatzbetriebspunkte)
                 .flatMap(List::stream)
                 .map(zbp -> zbp.getUicLand() + "_" + zbp.getNummer())
                 .forEach(valueCollector::addObject);
  }
}

您会注意到,我不仅返回了nummer字段,而且还包含了uicLand字段,这是我们真正想要的,但无法使用“ ... [any ] ...”符号。当然,如果我们想要与上面概述的行为完全相同的行为,我们只能返回数字。

现在,我们需要稍微修改hazelcast配置:

Config config = new Config();
final MapConfig mapConfig = config.getMapConfig("points");
//mapConfig.addMapIndexConfig(new MapIndexConfig("versionen[any].zusatzbetriebspunkte[any].nummer", false));
mapConfig.addMapIndexConfig(new MapIndexConfig("zusatzbetriebspunkt", false));
mapConfig.addMapAttributeConfig(new MapAttributeConfig("zusatzbetriebspunkt", BetriebspunktExtractor.class.getName()));

您会注意到,不再需要使用“ ... [any] ...”符号的“长”索引定义。

现在,我们可以使用此“伪属性”查询我们的值,而将对象添加到地图的顺序无关紧要:

Predicate keyPredicate = Predicates.equal("zusatzbetriebspunkt", "80_53090");
Collection<Betriebspunkt> keyResults = map.values(keyPredicate);
assertEquals(1, keyResults.size()); // always contains "abc"

现在,在我们的spring-data-hazelcast存储库中,我们可以执行以下操作:

@Query("zusatzbetriebspunkt=%d_%d")
List<StammdatenBetriebspunkt> findByZusatzbetriebspunkt(Integer uicLand, Integer nummer);

如果不需要使用spring-data-hazelcast,则可以直接返回BetriebspunktKey,然后在谓词中使用它,而不是将字符串返回给ValueCollector。那将是最干净的解决方案:

public class BetriebspunktExtractor extends ValueExtractor<Betriebspunkt, String> implements Serializable {
  @Override
  public void extract(final Betriebspunkt betriebspunkt, final String argument, final ValueCollector valueCollector) {
    betriebspunkt.getVersionen().stream()
                 .map(BetriebspunktVersion::getZusatzbetriebspunkte)
                 .flatMap(List::stream)
                 //.map(zbp -> zbp.getUicLand() + "_" + zbp.getNummer())
                 .forEach(valueCollector::addObject);
  }
}

然后

Predicate keyPredicate = Predicates.equal("zusatzbetriebspunkt", new BetriebspunktKey(80, 53090));

但是,要使其正常工作,BetriebspunktKey需要实现Comparable,并且还必须提供自己的equalshashCode方法。