我认为 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
orexpireAfterAccess
is requested entries may be evicted on each cache modification, on occasional cache accesses, or on calls toCache.cleanUp()
. Expired entries may be counted byCache.size()
, but will never be visible to read or write operations.
有一件事是,一旦过期,如果您尝试读取任何过期条目,您将看到它们不再存在。例如,尽管您没有看到 3000
条目在 RemovalListener
中被删除,但如果您调用 fooCache.get(3000)
,它必须首先加载该值(此时您会看到旧值被删除)。因此,从缓存 API 用户的角度来看,旧的缓存值已经消失了。
您在示例中看到特定行为的原因非常简单:出于并发原因,缓存是分段的。根据哈希码为条目分配一个段,每个段就像一个小型独立缓存。因此大多数操作(例如fooCache.get(1000)
)只会在单个段上操作。在您的示例中,1000
和 2000
明确分配给同一段,而 3000
位于另一个段中。在您的第二个版本中,4000
被分配给与 1000
和 2000
相同的段,因此它与其他两个版本一起被清理当写入 1000
的新值时。
在大多数实际使用中,段通常应该足够频繁地被命中,以便定期清理过期条目,从而不会成为问题。不过,除非您在缓存上调用 cleanUp()
,否则无法保证具体何时会发生。
关于java - 为什么缓存元素不会立即过期?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32056938/