我认为我只是简单地使用Guava缓存。但是,这种行为对我来说并不直观。我有一个POJO Foo
,其属性为Id
(Integer
)。在检索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;
}
}
}
答案 0 :(得分:3)
来自CacheBuilder
的Javadoc:
如果请求
expireAfterWrite
或expireAfterAccess
,则可能会在每次缓存修改,偶尔缓存访问或调用Cache.cleanUp()
时撤销条目。过期的条目可能由Cache.size()
计算,但在读取或写入操作时永远不可见。
有一件事是,一旦过期,如果您尝试阅读任何过期的条目,您将看到它们不再存在。例如,尽管您在3000
中没有看到RemovalListener
被删除的条目,但如果您调用fooCache.get(3000)
,则必须首先加载该值(并且你会看到当时删除旧值)。因此,从缓存API的用户的角度来看,旧的缓存值已经消失。
您在示例中看到特定行为的原因非常简单:出于并发原因,缓存是分段。条目根据其哈希码分配一个段,每个段的作用类似于一个小的独立缓存。因此,大多数操作(例如fooCache.get(1000)
)仅在单个段上运行。在您的示例中,1000
和2000
明确分配到同一个细分受众群,而3000
则位于另一个细分受众群中。在您的第二个版本中,4000
被分配到与1000
和2000
相同的细分受众群,因此在为{的新值写入时,它会与其他两个细分受到清理{1}}发生了。
在大多数实际使用中,细分通常应该经常受到打击,过期的条目将被定期清理到不成问题。但是,除非您在缓存上调用1000
,否则无法保证 时会发生。
答案 1 :(得分:2)
来自documentation(强调我的):
何时进行清理?
使用
CacheBuilder
构建的缓存不执行清理并“自动”或在值到期后立即或任何类型的任何内容立即驱逐值。 相反,它在写入操作期间执行少量维护,或者在写入很少的情况下偶尔执行读取操作。原因如下:如果我们想继续执行
Cache
维护,我们需要创建一个线程,其操作将与共享锁的用户操作竞争。此外,某些环境会限制线程的创建,这会使CacheBuilder
在该环境中无法使用。相反,我们将选择权交给您。如果您的缓存是高吞吐量,那么您不必担心执行缓存维护以清理过期的条目等。如果您的缓存很少写入并且您不希望清除阻止缓存读取,您可能希望创建自己的维护线程,定期调用
Cache.cleanUp()
。如果要为很少写入的缓存安排常规缓存维护,只需使用
ScheduledExecutorService
计划维护。
如果清理工作在您的系统中迅速发生,那么这些解决方案中的任何一个都应该适合您。
不相关,您可能已经知道这一点,但我希望您不要使用原始类型声明所有缓存类型。最好使用完全参数化的<Integer, Foo>
类型指定它们以防止heap pollution.