java - 为什么缓存元素不会立即过期?

标签 java caching guava

我认为 Guava 缓存的简单使用。然而,这种行为对我来说并不直观。我有一个 POJO,Foo ,其属性为 Id (Integer)。在检索 Foo 实例时,我使用 Integer 作为缓存的键。如果我将三个项目放入缓存中,并 hibernate 足够长的时间以使所有内容都过期,那么无论键值如何,我都会期望相同的行为。问题是我根据所使用的 key 看到不同的行为。我将三个对象放入缓存:1000、2000 和 3000。

[main] INFO CacheTestCase - 3000 creating foo, 1000
[main] INFO CacheTestCase - 3000 creating foo, 2000
[main] INFO CacheTestCase - 3000 creating foo, 3000
[main] INFO CacheTestCase - 3000 Sleeping to let some cache expire . . .
[main] INFO CacheTestCase - 3000 Continuing . . .
[main] INFO CacheTestCase - 3000 Removed, 1000
[main] INFO CacheTestCase - 3000 Removed, 2000
[main] INFO CacheTestCase - 3000 creating foo, 1000
[main] INFO CacheTestCase - 

请注意,在上面的运行中,键为 3000 的 Foo 实例并未从缓存中删除。下面是相同代码的输出,但我使用的 key 不是 3000,而是 4000。

[main] INFO CacheTestCase - 4000 creating foo, 1000
[main] INFO CacheTestCase - 4000 creating foo, 2000
[main] INFO CacheTestCase - 4000 creating foo, 4000
[main] INFO CacheTestCase - 4000 Sleeping to let some cache expire . . .
[main] INFO CacheTestCase - 4000 Continuing . . .
[main] INFO CacheTestCase - 4000 Removed, 1000
[main] INFO CacheTestCase - 4000 Removed, 2000
[main] INFO CacheTestCase - 4000 Removed, 4000
[main] INFO CacheTestCase - 4000 creating foo, 1000

当然,我做了一些极其愚蠢的事情。这是我的 MCVE:

package org.dlm.guava;

import com.google.common.cache.*;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

/**
 * Created by dmcreynolds on 8/17/2015.
 */
public class CacheTestCase {
    static final Logger log = LoggerFactory.getLogger("CacheTestCase");
    String p = ""; // just to make the log messages different
    int DELAY = 10000; // ms
    @Test
    public void testCache123() throws Exception {
        p = "3000";
        LoadingCache<Integer, Foo> fooCache = CacheBuilder.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(100, TimeUnit.MILLISECONDS)
                .removalListener(new FooRemovalListener())
                .build(
                        new CacheLoader<Integer, Foo>() {
                            public Foo load(Integer key) throws Exception {
                                return createExpensiveFoo(key);
                            }
                        });

        fooCache.get(1000);
        fooCache.get(2000);
        fooCache.get(3000);
        log.info(p + " Sleeping to let some cache expire . . .");
        Thread.sleep(DELAY);
        log.info(p + " Continuing . . .");
        fooCache.get(1000);
    }


    private Foo createExpensiveFoo(Integer key) {
        log.info(p+" creating foo, " + key);
        return new Foo(key);
    }


    public class FooRemovalListener
        implements RemovalListener<Integer, Foo> {
        public void onRemoval(RemovalNotification<Integer, Foo> removal) {
            removal.getCause();
            log.info(p+" Removed, " + removal.getKey().hashCode());
        }
    }

    /**
     * POJO Foo
     */
    public class Foo {
        private Integer id;

        public Foo(Integer newVal) {
            this.id = newVal;
        }

        public Integer getId() {
            return id;
        }
        public void setId(Integer newVal) {
            this.id = newVal;
        }
    }
}

最佳答案

来自 CacheBuilder 的 Javadoc :

If expireAfterWrite or expireAfterAccess is requested entries may be evicted on each cache modification, on occasional cache accesses, or on calls to Cache.cleanUp(). Expired entries may be counted by Cache.size(), but will never be visible to read or write operations.

有一件事是,一旦过期,如果您尝试读取任何过期条目,您将看到它们不再存在。例如,尽管您没有看到 3000 条目在 RemovalListener 中被删除,但如果您调用 fooCache.get(3000),它必须首先加载该值(此时您会看到旧值被删除)。因此,从缓存 API 用户的角度来看,旧的缓存值已经消失了。

您在示例中看到特定行为的原因非常简单:出于并发原因,缓存是分段的。根据哈希码为条目分配一个段,每个段就像一个小型独立缓存。因此大多数操作(例如fooCache.get(1000))只会在单个段上操作。在您的示例中,10002000 明确分配给同一段,而 3000 位于另一个段中。在您的第二个版本中,4000 被分配给与 10002000 相同的段,因此它与其他两个版本一起被清理当写入 1000 的新值时。

在大多数实际使用中,段通常应该足够频繁地被命中,以便定期清理过期条目,从而不会成为问题。不过,除非您在缓存上调用 cleanUp(),否则无法保证具体何时会发生。

关于java - 为什么缓存元素不会立即过期?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32056938/

相关文章:

java - 如何从动画 GIF 中获取长度(及时)

winapi - 如何检测程序执行过程中目录中的更改?

java - ImmutableMap 是大量键/对象/的次优选择吗?

java - 哪里可以找到正确的 Java 文档?

java - 独立 Wildfly 的 Microprofile 实现

java - 600851475143 的 "Integer number too large"错误消息

ruby-on-rails - Rails - 如何从片段缓存中排除代码块

javascript - javascript include 将被缓存多长时间?

java - Guava ForwardingList 使用示例

java - 将值放入谷歌 Guava loadingCache中