LoadingCache graphs
= CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener(MY_LISTENER)
.build( new CacheLoader() {
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
});
在实际情况下,缓存是非常有用的。例如,当一个值计算或检索起来所耗费的资源特别多,而又要不止一次的需要这个值,这个时候就应该考虑使用缓存。
一个 Cache
类似于 ConcurrentMap
, 但并不是全部一致. 最基本的区别是,ConcurrentMap
保存添加到它里面的所有元素,直至它们被显式的移除。 Cache
通常被配置为自动删除,以限制其过多占用内存。 在某些情况下 LoadingCache
的自动加载缓存功能是十分有用的。
简单来说, Guava 缓存工具在任何时候都适用:
如果需求适配与上述的情况,那么使用Guava 缓存工具将是一个不错的选择。
如上面的示例代码所示,使用CacheBuilder
完成构建缓存,但是但是定制缓存是最有趣的部分。
提示: 如果不需要 Cache的特性
, ConcurrentHashMap
的内存效率更高 -- 但是任何旧的ConcurrentMap复制Cache的大多数特性是极其困难或不可能的。
对于自己的缓存来说首要的问题是: 是否有一些合理的默认函数来加载或计算与键值关联的值? 如果确实如此,你应该使用 CacheLoader
. 如果没有,或者需要重新默认值,并且仍然需要原子语义 "get-if-absent-compute" , 那么应该将Callable
传递给 get
调用. 使用 Cache.put
可以直接替换掉元素, 但是自动缓存加载是首选的,因为它使得更容易推理所有缓存内容的一致性。
From a CacheLoader
LoadingCache
可以使用 CacheLoader
来构建。 创建一个 CacheLoader
只需要简单的实现 V load(K key) throws Exception
. 例如:
LoadingCache graphs = CacheBuilder.newBuilder() .maximumSize(1000) .build( new CacheLoader() {
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
});
...
try {
return graphs.get(key);
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
查询LoadingCache的典型方法是
get(K)
. 这个方法将返回缓存中已经存在的值,或者使用缓存的CacheLoader自动加载一个新的值进入缓存中。
因此 CacheLoader
可能抛出 Exception
, LoadingCache.get(K)
抛出 ExecutionException
. (如果缓存加载器抛出未检查异常, get(K)
将抛出包装它的 UncheckedExecutionException
) 你也可以选择使用 getUnchecked(K)
, 这个方法包装了所有的异常在 UncheckedExecutionException
里, 但是,底层CacheLoader
抛出检查异常是不符合惯例的。
LoadingCache graphs = CacheBuilder.newBuilder() .expireAfterAccess(10, TimeUnit.MINUTES) .build( new CacheLoader() {
public Graph load(Key key) {
// no checked exception
return createExpensiveGraph(key);
}
});
...
return graphs.getUnchecked(key);
getAll(Iterable extends K>)
可以用来做整体遍历。 默认情况下, getAll
将为缓存中不存在的每个key单独调用CacheLoader.load
. 当批量查找比单独查找更有效率时,可以重写 CacheLoader.loadAll
来利用此项. getAll(Iterable)
的性能将将相应提高。
请注意,您可以编写一个CaleloOrdel.Load实现,它为没有特定请求的键加载值。例如,如果计算来自某个组的任何密钥的值将给出组中所有密钥的值,loadAll可能同时加载组的其余部分。
From a Callable
所有 Guava 缓存, 加载与否, 都支持 get(K, Callable
方法。此方法返回与缓存中的键相关联的值,或者从指定的Callable计算该值并将其添加到缓存中。 与此缓存相关联的不可观察状态被修改,直到加载完成为止。该方法为传统的“如果缓存、返回、否则创建、缓存和返回”模式提供了简单的替代。
Cache cache = CacheBuilder.newBuilder() .maximumSize(1000) .build();
// look Ma, no CacheLoader ...
try {
// If the key wasn't in the "easy to compute" group, we need to
// do things the hard way.
cache.get(key, new Callable() {
@Override
public Value call() throws AnyException {
return doThingsTheHardWay(key);
}
});
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
Inserted Directly
值可以直接通过 cache.put(key, value)
插入缓存, 这将为指定键重写缓存中的任何以前的条目。还可以使用Cache.asMap() 视图
所暴露的ConcurrentMap的方法修改缓存。
注意,asMap
视图上的任何方法都不会导致条目被自动加载到缓存中。 此外,该视图上的原子操作在自动缓存加载范围之外操作,因此在缓存中使用CacheLoader或Callable加载值的缓存中,Cache.get(K,Callable
我们几乎可以确信的说,是没有足够的内存缓存我们想缓存的所有事情。你必须知道什么时候应该清理缓存。Guava提供了三种基本的驱逐方法:基于大小的驱逐,基于时间的驱逐和基于引用的驱逐。
如果想要缓存不应该超出一定的大小,请使用CacheBuilder.maximumSize(long)
方法。缓存将尝试驱逐出最近最常不被访问的实体。警告:当缓存大小接近极限,还没超出限制之前,可能会驱逐出部分实体。
或者, 如果不同的缓存条目具有不同的“权重”——例如,如果缓存值有完全不同的内存占用——则可以使用 CacheBuilder.weigher(Weigher)
指定权重,并使用 CacheBuilder.maximumWeight(long)
指定最大缓存权重。除了与maximumSize要求相同的警告之外,还要注意的是权重在条目创建时计算,之后是静态的。
LoadingCache graphs = CacheBuilder.newBuilder()
.maximumWeight(100000)
.weigher(new Weigher() {
public int weigh(Key k, Graph g) {
return g.vertices().size();
}
})
.build(
new CacheLoader() {
public Graph load(Key key) { // no checked exception
return createExpensiveGraph(key);
}
});
CacheBuilder
提供两种时间驱逐的方法:
expireAfterAccess(long, TimeUnit)
只有在最后一个读或写访问条目之后,指定的持续时间才会过期。请注意,条目被驱逐的顺序将类似于基于大小的驱逐。expireAfterWrite(long, TimeUnit)
在创建条目或最近发生值替换之后指定的持续时间过期。如果缓存的数据在一定时间后变长,这是可取的方法。定时到期是在写期间和偶尔读取期间以定期维护执行的,如下所述。
Testing Timed Eviction
测试时间过期方法不需要等待设置时的时长,使用 Ticker 接口与 CacheBuilder.ticker(Ticker)
方法在缓存生成器中指定时间源,而不必等待系统时钟。
Guava 允许设置垃圾收集器通过键的或值得弱引用,值得软引用进行条目驱逐。
CacheBuilder.weakKeys()
使用弱引用存储key值. 如果没有对key的其他(强或软)引用,则允许条目被垃圾收集。 由于垃圾收集仅依赖于标识相等,因此这导致整个缓存使用标识(==)相等来比较密钥,而不是使用equals()。CacheBuilder.weakValues()
使用弱引用存储value值. 如果没有对value的其他(强或软)引用,则允许条目被垃圾收集。 由于垃圾收集仅依赖于标识相等,因此这导致整个缓存使用标识(==)相等来比较密钥,而不是使用equals()。CacheBuilder.softValues()
将value用软引用包装。 响应于内存需求,软引用的对象以全局最少最近使用的方式被垃圾收集。由于使用软引用对性能有影响,我们通常建议使用更可预测的最大缓存大小。使用StuttValues()将导致使用(==)而不是 equals()
来比较值。在任何时候,您可以明确地取消缓存条目,而不是等待要删除的条目。这是可以做到的:
Cache.invalidate(key)
Cache.invalidateAll(keys)
Cache.invalidateAll()
当删除条目时通过CacheBuilder.removalListener(RemovalListener)
,可以指定缓存的移除监听器执行一些操作。
RemovalListener
通过一个指定删除原因,key,value的 RemovalNotification
执行移除操作。
注意,ReavalListInter抛出的任何异常都被记录(使用记录器)并被吞咽。
CacheLoader loader = new CacheLoader () {
public DatabaseConnection load(Key key) throws Exception {
return openConnection(key);
}
};
RemovalListener removalListener = new RemovalListener() {
public void onRemoval(RemovalNotification removal) {
DatabaseConnection conn = removal.getValue();
conn.close(); // tear down properly
}
};
return CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.removalListener(removalListener)
.build(loader);
Warning: 默认情况下,移除监听器操作是同步执行的,并且由于缓存维护通常在正常缓存操作期间执行,所以昂贵的移除侦听器会减慢正常缓存功能!如果有一个很耗费资源的删除监听器, 请使用 RemovalListeners.asynchronous(RemovalListener, Executor)
去装饰 RemovalListener
以进行异步操作。
使用CacheBuilder构建的缓存不会“自动”执行清理和删除值,或者在值过期后立即执行清除和删除值或任何此类操作。相反,它在写操作期间或偶尔读操作期间执行少量维护(如果写很少)。
原因如下:如果我们想要持续地执行缓存维护,我们需要创建一个线程,它的操作将与共享锁的用户操作竞争。此外,一些环境限制线程的创建,这将使CacheBuilder
无法在该环境中使用。
相反,我们把选择权交在你手中。如果缓存是高吞吐量的,那么就不必担心执行缓存维护来清理过期的条目等。如果您的高速缓存很少写入,并且不希望清理来阻止高速缓存读取,那么您可能希望创建自己的维护线程,该线程定期调用Cache.cleanUp()。
如果希望为很少有写入的缓存安排定期缓存维护,只需使用ScheduledExecutorService安排维护即可。
刷新与驱逐不一样。为指定的 LoadingCache.refresh(K)
,刷新key相对应的value值可能是异步的。旧值(如果有的话)在刷新键时仍然返回,这与强制检索等待直到重新加载该值时的强制驱逐形成对比。
如果刷新时抛出异常,则保留旧值,并记录并吞并异常。
CacheLoader可以通过重写CacheLoader.reload(K,V)指定刷新时使用的智能行为,这允许在计算新值时使用旧值。
// Some keys don't need refreshing, and we want refreshes to be done asynchronously.
LoadingCache graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(
new CacheLoader() {
public Graph load(Key key) { // no checked exception
return getGraphFromDatabase(key);
}
public ListenableFuture reload(final Key key, Graph prevGraph) {
if (neverNeedsRefresh(key)) {
return Futures.immediateFuture(prevGraph);
} else {
// asynchronous!
ListenableFutureTask task = ListenableFutureTask.create(new Callable() {
public Graph call() {
return getGraphFromDatabase(key);
}
});
executor.execute(task);
return task;
}
}
});
自动定时刷新可以使用CacheBuilder.refreshAfterWrite(long, TimeUnit)
添加到缓存中 . 与expireAfterWrite
相反,refreshAfterWrite
将使密钥在指定持续时间之后有资格进行刷新,但是只有在查询条目时才实际启动刷新。 (如果 CacheLoader.reload
是异步实现的, 查询不会因为刷新速度减慢。) 因此,例如,您可以在同一个缓存上指定refreshAfterWrite
和expireAfterWrite
,以便每当条目符合刷新条件时,条目上的过期计时器不会被盲目重置,因此,如果条目在符合刷新条件之后未被查询,则允许其过期。
使用 CacheBuilder.recordStats()
, 可以通过Guava cache返回静态集合。 Cache.stats()
方法返回一个 CacheStats
对象, 其提供的静态方法有:
hitRate()
, 返回请求命中的比率averageLoadPenalty()
, 加载新值的平均时间,单位为纳秒evictionCount()
, 缓存驱逐数量除此之外还有更多的统计数据。这些统计信息对于缓存优化至关重要,我们建议在关键性能的应用程序中注意这些统计信息。
asMap
可以使用asMap视图将任何Cache作为为ConcurrentMap查看,但是asMap视图如何与Cache交互需要一些解释。
cache.asMap()
包含当前在缓存中加载的所有项。 例如, cache.asMap().keySet()
包含当前加载的所有key.asMap().get(key)
与 cache.getIfPresent(key)
相似,并且永远不会导致加载值,这与Map
是一致的。Cache.asMap().get(Object)
与 Cache.asMap().put(K, V)
), 但是不包括 containsKey(Object)
, 也不通过操作Cache.asMap()
集合视图 .因此,通过 cache.asMap().entrySet()
不会重置检索的条目的访问时间。加载方法 (像get
) 不会抛出 InterruptedException
. 可以设计一些方法支持InterruptedException
, 但我们的支持将是不完整的,迫使它的成本对所有用户,但它的好处只有一些。详情请继续阅读。
get
要求未收回价值的请求分为两大类: 那些加载值和等待另一线程正在进行加载的那些。 我们支持中断的能力是不同的。简单的情况是等待另一个线程正在进行的加载:在这里我们可以输入一个可中断的等待。 难点是我们自己加载的值。 在这里,我们让用户自定义CacheLoader。如果碰巧支持中断,我们可以支持中断;如果不是,我们不能。
那么,为什么CacheLoader
工作时不支持中断呢? 在某种意义上,我们这样做(但见下文):如果 CacheLoader
抛出 InterruptedException
, 所有 get
对key的调用将迅速返回(与任何其他异常一样)。 而且, get
将恢复加载线程中的中断位。 这时,令人惊讶的是 InterruptedException
被包装成了 ExecutionException
.
原则上,我们可以为您打开这个异常。 然而,这将强制LoadingCache使用时处理InterruptedException。
即使CacheLoader
的实现从来不会抛出这个异常。 当你认为所有非加载线程的等待仍然会被中断时,这可能还是值得的。 但是许多缓存只在单个线程中使用。 他们的使用这仍然必须抓住可能的 InterruptedException
. 甚至那些跨线程共享缓存的用户有时也能够中断他们的get调用,基于哪个线程首先发出请求。
我们在这个决定中的指导原则是让缓存表现为好像所有的值都被加载到调用线程中。这一原理使得很容易将缓存引入到以前在每个调用中重新计算其值的代码中。如果旧代码是不可中断的,那么新代码也可能是不可中断的。
我说过“在某种意义上”我们支持中断。还有一种意义上我们并不支持中断,那就是使LoadingCache成为一个有漏洞的抽象。如果加载线程被中断,我们会像任何其他异常一样对待它。这在很多情况下是好的,但是当多个get调用等待值时,这不是正确的事情。虽然碰巧正在计算值的操作被中断,但是其他需要该值的操作可能没有中断。然而,所有这些调用者都接收到Inter.edException(包装在ExecutionException中),即使负载并没有“失败”到“中止”。对此我们有一个漏洞。然而,修复可能是有风险的。我们可以将额外的工作投入到建议的AsyncLoadingCache中,而不是解决问题,AsyncLoadingCache将返回具有正确中断行为的Future对象。