【问题标题】:Why don't cache elements expire promptly?为什么缓存元素不会立即过期?
【发布时间】:2015-08-18 04:32:30
【问题描述】:

我有一个我认为是 Guava 缓存的简单用途。但是,这种行为对我来说并不直观。我有一个 POJO,Foo,属性为 Id (Integer)。在检索Foo 的实例时,我使用Integer 作为缓存的键。如果我将三个项目放入缓存中,并休眠足够长的时间以使所有内容都过期,那么无论键值如何,我都会期望相同的行为。问题是我根据使用的密钥看到不同的行为。我将三个对象放入缓存中: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 实例并未从缓存中删除。下面是相同代码的输出,但我使用了 4000 而不是 3000 的键。

[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;
        }
    }
}

【问题讨论】:

    标签: java caching guava


    【解决方案1】:

    来自CacheBuilder 的 Javadoc:

    如果expireAfterWriteexpireAfterAccess 被请求,条目可能会在每次缓存修改、偶尔的缓存访问或调用Cache.cleanUp() 时被驱逐。过期条目可能会被 Cache.size() 计数,但永远不会对读取或写入操作可见。

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

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

    在大多数实际使用中,段通常应该经常被命中,过期的条目会被定期清理到不会成为问题的程度。但是,不能保证确切何时会发生,除非你在缓存上调用cleanUp()

    【讨论】:

      【解决方案2】:

      一旦发生超时,维护不会立即进行。

      来自documentation(强调我的):

      什么时候进行清理?

      使用CacheBuilder 构建的缓存不会“自动”执行清理和逐出值,也不会在值过期后立即执行或任何类似操作。 相反,它会在写入操作期间执行少量维护,如果写入很少,则在偶尔的读取操作期间执行。

      原因如下:如果我们想连续进行Cache维护,我们需要创建一个线程,它的操作会与用户操作竞争共享锁。此外,某些环境会限制线程的创建,这会使CacheBuilder 在该环境中无法使用。

      相反,我们将选择权交给您。如果您的缓存是高吞吐量的,那么您不必担心执行缓存维护以清理过期条目等。如果您的缓存很少写入并且您不希望清理以阻止缓存读取,您可能希望创建自己的维护线程,定期调用Cache.cleanUp()

      如果您想为很少写入的缓存安排定期缓存维护,只需使用ScheduledExecutorService 安排维护。

      如果在您的系统中及时进行清理很重要,这些解决方案中的任何一个都应该适合您。


      无关,您可能已经知道这一点,但我希望您没有使用原始类型声明所有缓存类型。最好使用完全参数化的&lt;Integer, Foo&gt; 类型来指定它们,以防止heap pollution. 的风险

      【讨论】:

        猜你喜欢
        • 2019-08-26
        • 1970-01-01
        • 1970-01-01
        • 2019-02-12
        • 2020-12-04
        • 2019-06-18
        • 2021-05-09
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多