如何避免在值为null时进行缓存?

61

我正在使用Guava缓存热数据。当缓存中不存在数据时,我需要从数据库获取:

public final static LoadingCache<ObjectId, User> UID2UCache = CacheBuilder.newBuilder()
        //.maximumSize(2000)
        .weakKeys()
        .weakValues()
        .expireAfterAccess(10, TimeUnit.MINUTES)
        .build(
        new CacheLoader<ObjectId, User>() {
            @Override
            public User load(ObjectId k) throws Exception {
                User u = DataLoader.datastore.find(User.class).field("_id").equal(k).get();
                return u;
            }
        });

我的问题是当数据在数据库中不存在时,我希望它返回null而不缓存任何内容。但Guava使用该键将null保存在缓存中,并在获取它时抛出异常:

com.google.common.cache.CacheLoader$InvalidCacheLoadException: CacheLoader返回了键shisoft的空值。

我们如何避免缓存null值?


17
请注意,缓存空值可能会节省大量的数据库访问次数,当然,这取决于您的访问模式。因此,在仔细考虑之后,我不会拒绝对它们进行缓存。 - maaartinus
5
Guava在缓存中不会保存带有空键的值,而是会抛出异常。 - bylijinnan
2
一个有趣的小细节:如果你碰巧在代码中使用了 RuntimeExcpetions,Guava 会将其重新封装为 com.google.common.util.concurrent.UncheckedExecutionException;-| - Andrew Norman
5个回答

84
如果找不到用户,只需抛出一些异常,并在使用get(key)方法时在客户端代码中捕获它。
new CacheLoader<ObjectId, User>() {
    @Override
    public User load(ObjectId k) throws Exception {
        User u = DataLoader.datastore.find(User.class).field("_id").equal(k).get();
        if (u != null) {
             return u;
        } else {
             throw new UserNotFoundException();
        }
    }
}

以下是来自CacheLoader.load(K) Javadoc的内容:

Returns:  
  the value associated with key; must not be null  
Throws:  
  Exception - if unable to load the result

回答关于缓存空值的疑问:

返回与此缓存中键相关联的值,必要时首先加载该值。在加载完成之前,不会修改与此缓存相关联的任何可观察状态

(来自LoadingCache.get(K) Javadoc)

如果抛出异常,则不认为加载已完成,因此不会缓存新值。

编辑

请注意,在Caffeine中,这是一种类似于Guava cache 2.0并“使用Google Guava启发式API提供内存中缓存”的方法,您可以从load方法中返回null

 Returns:
   the value associated with key or null if not found
如果您考虑迁移,数据加载器可以自由返回,当用户未找到时。

3
您可以捕获 ExecutionException,并且其原因应该是 UserNotFoundException(当然,在 .getCause() 后面应该进行 instanceof 检查以确保它不是另一个已检查的异常)。或者,如果您不喜欢已检查的异常,可以使用 getUnckecked,或者如果 get 不是您所需的,则可以使用 getIfPresent - Grzegorz Rożniecki
4
这违反了《Effective Java》第69条:只在异常情况下使用异常。 - Adam Bliss
@AdamBliss 我不同意,就Guava的 LoadingCache 合约而言这是一个特殊情况 —— 你“无法加载结果”,因此抛出异常。另外,第55条(关于返回 Optional)指出“简而言之,如果您发现自己编写的方法不能始终返回值(...) 对于性能关键的方法,最好返回 null 或抛出异常。” - Grzegorz Rożniecki
@Xaerxess 我同意“无法加载结果”从缓存的角度来看是异常的。但是“数据库中没有条目”从加载器的角度来看并不是异常,这就是问题所在。这实际上意味着LoadingCache合同不太适用于此用例。可以在LoadingCache<Optional<T>>之上实现“PartialCache<T>”,但我认为使用invalidate()比throw/catch更好。 - Adam Bliss
1
@AdamBliss 无法反驳Guava缓存的设计决策,特别是Caffeine,它是Guava缓存的继承者,允许从CacheLoader#load(K)返回null作为“未找到值”的表示。我已编辑了我的回答并提到了Caffeine缓存。 - Grzegorz Rożniecki
显示剩余5条评论

59

简单的解决方案:使用com.google.common.base.Optional<User>而不是User作为值。

public final static LoadingCache<ObjectId, Optional<User>> UID2UCache = CacheBuilder.newBuilder()
        ...
        .build(
        new CacheLoader<ObjectId, Optional<User>>() {
            @Override
            public Optional<User> load(ObjectId k) throws Exception {
                return Optional.fromNullable(DataLoader.datastore.find(User.class).field("_id").equal(k).get());
            }
        });

编辑:我认为@Xaerxess的回答更好。


7
技术上它会缓存“null”值(即 Optional.absent() 对象),因此它并不能像 OP 想要的那样避免缓存 null 值,但成本不高。 - Grzegorz Rożniecki
没关系。我的问题略有不同,但你的解决方案很有帮助。 ;) - Haroldo_OK
我认为这个答案应该被删除。不是因为它在某些情况下不可行 - 而是因为它可能会让读者感到困惑,因为它缓存了不存在的值(与问题中的要求相反)。 - Gilbert
8
使用 cache.get(key) 方法时,如果返回的是 Optional.absent,你可以直接使用 cache.invalidate(key) 来确保 Optional.absent 只存在于该次 get 调用的生命周期中,然后它就会消失。 - tolitius
7
使用Optional.absent()存储对于想要避免每次重新加载不存在值的情况非常有用,而是与其他缓存值具有相同的过期时间框架一起使用,这很适合Optional! - centic

5

面对相同的问题,因为源中缺少值是正常工作流程的一部分。我没有找到比自己编写代码使用 getIfPresentgetput 方法更好的方法了。请查看下面的方法,其中 localCache<Object, Object>:

private <K, V> V getFromLocalCache(K key, Supplier<V> fallback) {
    @SuppressWarnings("unchecked")
    V s = (V) local.getIfPresent(key);
    if (s != null) {
        return s;
    } else {
        V value = fallback.get();
        if (value != null) {
            local.put(key, value);
        }
        return value;
    }
}

1
问题是可以对“fallback.get()”进行许多相同键的并行调用,导致计算相同的值。在某些情况下,这可能会非常耗费资源。 - AlikElzin-kilaka
是的。这就是答案。Guava LoadingCache由于其糟糕的null处理而毫无价值。 - ccleve

0

当您想要缓存一些NULL值时,您可以使用其他行为类似于NULL的工具。

在提供解决方案之前,我建议您不要将LoadingCache暴露给外部。相反,您应该使用方法来限制缓存的范围。

例如,您可以使用LoadingCache<ObjectId,List<User>>作为返回类型。然后,当您无法从数据库中检索到值时,您可以返回空列表。您可以使用-1作为Integer或Long的NULL值,您可以使用""作为String的NULL值等。完成此操作后,您应该提供一个处理NULL值的方法。

when(value equals NULL(-1|"")){
   return null;
}

1
不,如果您需要此功能,请使用java.util.Optional类。另请参见@卢声远Shengyuan Lu的答案和评论。 - Jacob van Lingen

0

我使用getIfPresent

@Test
    public void cache() throws Exception {
        System.out.println("3-------" + totalCache.get("k2"));
        System.out.println("4-------" + totalCache.getIfPresent("k3"));
    }



    private LoadingCache<String, Date> totalCache = CacheBuilder
            .newBuilder()
            .maximumSize(500)
            .refreshAfterWrite(6, TimeUnit.HOURS)
            .build(new CacheLoader<String, Date>() {
                @Override
                @ParametersAreNonnullByDefault
                public Date load(String key) {
                    Map<String, Date> map = ImmutableMap.of("k1", new Date(), "k2", new Date());
                    return map.get(key);
                }
            });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接