怎么可能某个key存在于缓存中却无法通过cache.get(key)检索到呢?

问题描述 投票:0回答:1

背景: 有一个 Infinispan (13.0.10) 缓存,它使用自定义数据类型

Quad<String, Long, Type, ValidityPeriod>
作为键。据我所知,所有
hashCode()
equals(Object o)
方法要么是自动生成的,要么是正确实现的。 缓存也用于本地模式。

问题: 当我尝试使用我知道缓存包含的 Quad 来调用

cache.get(Object key)
时,我得到
null
cache.containsKey(Object key)
也返回
false

这怎么可能?

澄清: 我说我知道缓存包含

Quad
键,因为我已经使用 Eclipse IDE 调试器完成了以下操作:

cache.entrySet().stream().filter(e -> e.getKey().hashCode == Quad.of(a, b, c, d).hashCode()).collect(toList());
// this returns a non-empty list with my expected return
cache.entrySet().stream().filter(e -> e.getKey().equals(Quad.of(a, b, c, d))).collect(toList());
// this returns a non-empty list with my expected return

根据 Radim Vansa 的建议,我还尝试了以下方法:

cache.entrySet().stream().collect(Collectors.toMap(Entry::getKey, Entry::getValue, (o1,o2) -> o1, ConcurrentHashMap::new)).get(Quad.of(a, b, c, d));
// this returns my expected return

更多背景信息(如果需要): 我正在开发一个较旧的项目,我应该将其从 Infinispan 版本 10 更新到 13。我已经成功地做到了这一点,并且还集成了 ProtoStream API,而不是使用旧的 JBossMarshaller。此版本更改很重要,因为更新后检索停止工作。

有多个缓存正在使用,有些缓存使用自定义数据类型进行索引,例如

Pair<K, V>
Triple<L, M, R>
Quad<A, B, C, D>
,这些都是通用的。我已经为它们编写了一些 ProtoAdpaters,它们工作得很好。

我现在遇到了一个问题,其中有一个 AdvancedCache 使用这样的 Quad 作为键:

Quad<String, Long, Type, ValidityPeriod>

Quad 覆盖

equals(Object o)
hashCode
,如下所示:

public class Quad<A, B, C, D> {

    // other code omitted for brevity

    public boolean equals(final Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj instanceof Quad<?, ?, ?, ?>) {
            final Quad<?, ?, ?, ?> other = (Quad<?, ?, ?, ?>) obj;
            return Objects.equals(getFirst(), other.getFirst())
                && Objects.equals(getSecond(), other.getSecond())
                && Objects.equals(getThird(), other.getThird())
                && Objects.equals(getFourth(), other.getFourth());
        }
        return false;
    }

    public int hashCode() {
        return Objects.hashCode(getFirst()) 
                ^ Objects.hashCode(getSecond()) 
                ^ Objects.hashCode(getThird()) 
                ^ Objects.hashCode(getFourth());
    }
}

仅供参考

Type
的结构如下:

public class Type implements Serializable {
    private int fieldA;
    private int fieldB;
    private String fieldC;

    // hashCode and equals are auto-generated
    // constructor, getters and setters omitted for brevity
}

ValidityPeriod
是这样的:

public class ValidityPeriod implements Serializable {
    private LocalDate validFrom;
    private LocalDate invalidFrom;

    // hashCode and equals are auto-generated
    // constructor, getters and setters omitted for brevity
}

LocalDate
的编组器使用以下适配器:

@ProtoAdapter(LocalDate.class)
public class LocalDateAdapter {
    
    @ProtoFactory
    LocalDate create(int year, short month, short day) {
        return LocalDate.of(year, month, month);
    }
    
    @ProtoField(number = 1, required = true)
    int getYear(LocalDate localDate) {
        return localDate.getYear();
    }
    
    @ProtoField(number = 2, required = true)
    short getMonth(LocalDate localDate) {
        return (short) localDate.getMonth().getValue();
    }
    
    @ProtoField(number = 3, required = true)
    short getDay(LocalDate localDate) {
        return (short) localDate.getDayOfMonth();
    }
}

我尝试使用调试器来了解 Infinispan 的内部工作原理,但我似乎无法确定产生此错误的具体线路。 据我所知,这与

CacheImpl.get(Object, long, InvocationContext)
有关。

更新: 好的,根据 Pruivo 的建议,我尝试创建一个 reprex。然而奇怪的是,我试图复制从缓存检索对象之前发生的每个过程,但在我创建的 reprex 中它可以工作。 然而有趣的是我尝试了以下内容: 我在

ValidityPeriod
中创建了两个方法,它们执行以下操作(几乎像 Infinispan Transformers):

public String toFormString() {
    return String.format("%s§%s", validFrom, invalidFrom);
}
    
public static ValidityPeriod fromFormString(String form) {
    String[] split = form.split("§");
    return from(LocalDate.parse(split[0]),LocalDate.parse(split[1]));
}

然后,我将 Quad 更改为

Quad<String, Long, Type, String>
,同时使用这些方法中的字符串而不是 ValidityPeriod 本身构建缓存的密钥。奇怪的是,这解决了我原来的问题。 然而,由于这是一个肮脏的修复,我不满足于永久保留此解决方案。我猜一定是
ValidityPeriod
出了问题。

我仍然很困惑为什么缓存返回的内容与它包含的内容不同,所以我仍然会保留我原来的问题。

java caching infinispan
1个回答
3
投票

按照 Pruivo 的建议仔细推论后,我可以确定这个错误的原因。它是为

ProtoAdapter
编写的
LocalDate
,其中在反序列化时创建了不正确的
LocalDate
对象。

具体修复: 这种情况的具体修复如下:

@ProtoAdapter(LocalDate.class)
public class LocalDateAdapter {
    
    @ProtoFactory
    LocalDate create(int year, short month, short day) {
        return LocalDate.of(year, month, day); // <= here was the error
    }
    
    @ProtoField(number = 1, required = true)
    int getYear(LocalDate localDate) {
        return localDate.getYear();
    }
    
    @ProtoField(number = 2, required = true)
    short getMonth(LocalDate localDate) {
        return (short) localDate.getMonth().getValue();
    }
    
    @ProtoField(number = 3, required = true)
    short getDay(LocalDate localDate) {
        return (short) localDate.getDayOfMonth();
    }
}

说明: 正如 Pruivo 所说,Infinispan 使用

hashCode()
来确定密钥的所有者/段。在后台,Infinispan 使用
ConcurrentHashMap
来存储键和值。如果缓存具有相应的配置参数集,则此映射包含序列化对象。

现在来说说问题。

ConcurrentHashMap
包含调用以下方法来检索对象:

public V get(Object key) {
   Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
   int h = spread(key.hashCode());
   if ((tab = table) != null && (n = tab.length) > 0 &&
      (e = tabAt(tab, (n - 1) & h)) != null) { // <= line 5
         // this code here is not reached
         if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
         }
      }
   }
   return null;
}

正如您在第 5 行中看到的,有一个名为

tabAt()
的方法的检查。这验证了地图该地址处的对象引用不是
null
。如果是
null
,则不采取进一步操作,并且将返回
null
。这包括不使用对象的哈希值或
equals
进行比较。您可以在
tabAt()
之后的行中看到这一点。这可以解释为什么
cache.entrySet().stream()...
可以检索对象而
get()
却不能。

现在我有根据地猜测为什么找不到对象的引用,因此是

null
:我猜原始密钥是正确序列化的对象的密钥,但在反序列化后,该密钥变得损坏,因为它通过使用错误的对象更改了值编组员。由于修改
Map
中的键可能会产生意外的行为,这可以解释奇怪的行为。 (修改密钥时
Map
意外行为的来源:https://stackoverflow.com/a/21601013/14945497

建议/备注: 我想说的最后一点是,这个错误可能非常不明显,但会导致其他问题,因为反序列化时会创建错误的对象。这会造成很多与奇怪行为的混淆,并且需要做大量的侦探工作来消除它。考虑到这一点,我的建议是对所有编组器的定义进行双重检查。

© www.soinside.com 2019 - 2024. All rights reserved.