为什么缓存元素不会及时过期?

时间:2015-08-17 18:07:48

标签: java caching guava

我认为我只是简单地使用Guava缓存。但是,这种行为对我来说并不直观。我有一个POJO Foo,其属性为IdInteger)。在检索Integer的实例时,我使用Foo作为缓存的键。如果我在缓存中放入三个项目,并且睡眠时间足够长以使一切都过期,无论键值如何,我都会期望相同的行为。问题是我根据使用的密钥看到了不同的行为。我在缓存中得到三个对象: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;
        }
    }
}

2 个答案:

答案 0 :(得分:3)

来自CacheBuilder的Javadoc:

  

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

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

您在示例中看到特定行为的原因非常简单:出于并发原因,缓存是分段。条目根据其哈希码分配一个段,每个段的作用类似于一个小的独立缓存。因此,大多数操作(例如fooCache.get(1000))仅在单个段上运行。在您的示例中,10002000明确分配到同一个细分受众群,而3000则位于另一个细分受众群中。在您的第二个版本中,4000被分配到与10002000相同的细分受众群,因此在为{的新值写入时,它会与其他两个细分受到清理{1}}发生了。

在大多数实际使用中,细分通常应该经常受到打击,过期的条目将被定期清理到不成问题。但是,除非您在缓存上调用1000,否则无法保证 时会发生

答案 1 :(得分:2)

超时发生后,维护不会立即发生。

来自documentation(强调我的):

  

何时进行清理?

     

使用CacheBuilder构建的缓存不执行清理并“自动”或在值到期后立即或任何类型的任何内容立即驱逐值。 相反,它在写入操作期间执行少量维护,或者在写入很少的情况下偶尔执行读取操作。

     

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

     

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

     

如果要为很少写入的缓存安排常规缓存维护,只需使用ScheduledExecutorService计划维护。

如果清理工作在您的系统中迅速发生,那么这些解决方案中的任何一个都应该适合您。

不相关,您可能已经知道这一点,但我希望您不要使用原始类型声明所有缓存类型。最好使用完全参数化的<Integer, Foo>类型指定它们以防止heap pollution.

的风险