缓存
示例
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener(MY_LISTENER)
.build(
new CacheLoader<Key, Graph>() {
@Override
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
});
适用性
Caches在大量各种各样的用例中都是非常地有用的。例如,当一个值对于计算或者检索是代价高昂的,你应该考虑使用缓存,并且你将不止一次在某输入上需要它的值。
Cache
类似于ConcurrentMap
,但不完全相同。最根本的不同是ConcurrentMap
会保留所有添加的元素直到他们显性地移除。Cache
另一方面通常配置为自动化驱逐条目,为了限制它的内存占用。在一些情况下LoadingCache
是非常有用的,尽管它不会驱逐条目,由于它的自动化缓存载入。
通常,Guava缓存工具适用于以下情况:
- 你将消耗一些内存来提升速度。
- 你期望键(key)有时候查询不止一次。
- 你的缓存不需要存储超过RAM容量的数据。(Guava缓存是你的应用程序单独运行的本地程序。他们不会存储数据到文件或者在外部服务器。如果这个不符合你的要求,考虑像Memcached这样的工具。)
如果这些每一个都适用你的用例,那么Guava缓存工具对你了说是对的。
使用CacheBuilder
构造器模式完成的获取Cache
通过上面的示例代码展示了,但是定制你的缓存是有意思的部分。
注意:如果你不需要Cache
的特性,ConcurrentHashMap
是更加内存高效的 – 但是如果使用任何旧的ConcurrentMap
复制大多数Cache
特性是极其困难的或者不可能的。
整体
问你自己关于缓存的第一个问题是:是否有合理的默认功能加载或者计算一个关联key的值?如果有,你应该使用CacheLoader
。如果没有,或许你需要重载默认实现,但是如果你仍想要自动化 “get-if-absent-compute”[获取-如果不存在则填充]语义,你应该传入Callable
到get
调用。使用Cache.put
,可以直接插入元素,但是自动化缓存加载是优先选择的,因为它更容易推断所有缓存内容的一致性。
从CacheLoader获取
LoadingCache
是用附加的CacheLoader
构建的Cache
。创建CacheLoader
通常和实现方法V load(K key) throws Exception
一样容易。所以,例如,你可以使用以下代码创建LoadingCache
:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(
new CacheLoader<Key, Graph>() {
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
封装了所有异常,但是如果底层的CahceLoader
要是抛出检查异常,这可能导致意外行为。
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build(
new CacheLoader<Key, Graph>() {
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)
的性能将相应地提高。
注意,你可以写一个CacheLoader.loadAll
实现来加载那些没有特意请求的键的值。例如,如果计算某个组的任意键的值可以得到组中的所有键的值,那么loadAll
可能同时加载该组的其他键。
从Callable获取
所有Guava缓存,加载或者没有加载,支持方法get(K, Callable<V>)
。这个方法返回在缓存中与key关联的值,或者从指定的Callable
计算并将它添加到缓存。不会修改与这个缓存相关联的可观察的状态,直到加载完成。这个方法提供常规的"if cached,retun;otherwise create,cache and return"格式的一个简单的替代方案。
Cache<Key, Value> 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<Value>() {
@Override
public Value call() throws AnyException {
return doThingsTheHardWay(key);
}
});
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
直接插入
使用cache.put(key, value)
值可以直接插入到缓存中。这将覆盖缓存中指定的key的任何先前的条目。使用通过Cache.asMap
视图暴漏的ConcurrentMap
的任何方法都会造成修改。注意在asMap
视图上没有方法导致条目自动加载到缓存中。此外,在此视图的原子操作在自动缓存加载范围之外操作,因此,在使用CacheLoader
或者Callable
加载值得缓存中,Cache.get(K, Callable<V>)
应该一直优先于Cache.asMap().putIfAbsent()
。注意Cache.get(K, Callable)
也可以插入值到底层缓存。
回收
冷酷的现实是我们差不多确认不会有足够的内存来缓存我们想要缓存的所有东西。你必须决定:什么时候不值得保存缓存条目?Guava提供三个基本类型的回收:基于大小回收,基于时间回收和基于引用回收。
基于大小回收
如果你的缓存不会增长到某个大小之上,可以使用CacheBuilder.maximumSize(long)
。缓存将尝试回收最近没有使用或者不经常使用的条目。警告:缓存可能在限制溢出之前回收条目–通常当这个缓存大小将近这个限制的时候。
或者,如果不同的缓存条目有不同的"权重" – 例如,如果你的缓存值具有完全不同的内存占用–你可以指定使用CacheBuilder.weigher(Weigher)
权重功能和使用CacheBuilder.maximumWeight(long)
指定最大的缓存权重。除了与maximumSize
要求的注意事项之外,还要注意权重是条目创建时计算的,之后是静态的。
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumWeight(100000)
.weigher(new Weigher<Key, Graph>() {
public int weigh(Key k, Graph g) {
return g.vertices().size();
}
})
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) { // no checked exception
return createExpensiveGraph(key);
}
});
定时回收
CacheBuilder
针对定时回收提供两种方式:
expireAfterAccess(long, TimeUnit)
只有自上一次读或者写的访问以来的指定时间之后才能使条目过期。注意在这种情况下的条目被回收的顺序类似于基于大小回收。expireAfterWrite(long, TimeUnit)
自条目被创建或者值最近被替换以来传入的指定时间之后使条目过期。如果一定时间后缓存的数据变得陈旧这是可取的。
定时过期是在写期间或者偶尔在读期间执行,正如以下所讨论的。
测试定时回收
测试定时回收并不一定是痛苦的…并且不会消耗你两秒钟来测试两秒过期。使用Ticker接口和CacheBuilder.ticker(Ticker)
方法在你的缓存构造器中指定一个时间源,而不是必须等待系统时间。
基于引用回收
通过使用key或者值的弱引用,和通过使用值的软引用,Guava允许你设置你的缓存来允许条目的垃圾回收器收集。
CacheBuilder.weakKeys()
使用弱引用存储key。如果没有其他引用(强或者软引用)这个key,这允许条目被垃圾收集器收集。由于垃圾收集器依赖于标识相等,这将导致整个缓存使用统一性(==)等价来比较key,而不是equals()
。CacheBuilder.weakValues()
使用弱引用存储值。如果没有其他引用(强或者软引用)这个值,这允许条目被垃圾收集器收集。因为垃圾收集器依赖于标识相等,这将导致全部缓存使用统一性(==)等价来比较值,而不是equals()
。CacheBuilder.softValues()
使用软引用封装值。软引用使用全局的LRU(最近最少使用)方式垃圾回收,以响应内存需求。由于使用软引用对性能的影响,我们通常建议使用更加可预测的最大值缓存大小代替。softValues()
的使用将导致值使用统一性(==
)等价进行比较,而不是equals()
。
显性清除
在任何时间,你可以显性地使缓存条目无效,而不是等等条目被回收。这可以做到:
- 单独地,使用
ache.invalidate(key)
- 批量,使用
Cache.invalidateAll(keys)
- 对所有条目,使用
Cache.invalidateAll()
[清除(Removal)]监听器
你可以为缓存指定一个清除监听器,当移除一个条目时,来执行一些操作,通过CacheBuilder.removalListener(RemovalListener)
。RemovalListener
获取传入的RemovalNotification
,其指定RemovalCause
,键和值。
注意:通过RemovalListener
抛出的任何异常会被记录(使用Logger
)并被内部处理。
CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
public DatabaseConnection load(Key key) throws Exception {
return openConnection(key);
}
};
RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
DatabaseConnection conn = removal.getValue();
conn.close(); // tear down properly
}
};
return CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.removalListener(removalListener)
.build(loader);
警告:清除监听器操作默认情况下同步执行,由于缓存维护通常在正常缓存操作期间执行,因此代价高昂的清除监听器可降低正常缓存功能的速度!如果你有一个代价高昂的移除监听器,使用RemovalListeners.asynchronous(RemovalListener, Executor)
适配RemovalListener
来异步操作。
什么时候发生清除(Cleanup)
使用CacheBuilder
构建的缓存不会”自动化“执行清除和回收值,在值过期后也不会或者类似的事情。相反,在操作期间会执行少量维护或者在写很少偶尔执行读操作期间。
正如以下原因:如果我们想要持续执行Cache
维护,我们需要创建一个线程,并且它的操作可能与用户操作在共享锁的情况下产生竞争。除此之外,一些环境约束线程的创建,其可能导致CacheBuilder
在该环境中不可用。
然而,我们将这种选择交给你。如果你的缓存高吞吐量,然后你不必担心执行缓存维护来清除过期条目等等。如果你的缓存很少写并且你不希望清除来阻塞缓存读,你可能希望创建你自己的维护线程,并定期调用Cache.cleanUp()
。
如果你想要对一个仅有少量写操作的缓存进行计划定期的缓存维护,仅需要使用ScheduledExecutorService
进行计划维护。
刷新
刷新与回收不太一样。正如在LoadingCache.refresh(K)
所指定的,刷新一个键会为该键加载一个新值,可能异步处理。当键正在刷新时,老的值(如果有的话)仍会返回,与驱逐相反,其强制检索时等待直到值再次加载。
当刷新时如果抛出异常,保留老值并且记录异常并内部处理。
CacheLoader
可以通过重载CacheLoader.reload(K, V)
在刷新上指定一个明智的行为来使用,它允许你在计算新值时使用老值。
// Some keys don't need refreshing, and we want refreshes to be done asynchronously.
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) { // no checked exception
return getGraphFromDatabase(key);
}
public ListenableFuture<Graph> reload(final Key key, Graph prevGraph) {
if (neverNeedsRefresh(key)) {
return Futures.immediateFuture(prevGraph);
} else {
// asynchronous!
ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() {
public Graph call() {
return getGraphFromDatabase(key);
}
});
executor.execute(task);
return task;
}
}
});
可以使用CacheBuilder.refreshAfterWrite(long, TimeUnit)
将自动化定时刷新添加到缓存。与expireAfterWrite
不同的是,expireAfterWrite
在指定的时间后符合刷新条件,但是刷新仅在条目在查询时被实际启用。(如果CacheLoader.reload
是异步实现,刷新将导致查询效率变低。)所以,例如,你可以在同一个缓存上同时指定refreshAfterWrite
和expireAfterWrite
,以便,当条目上有资格刷新时,条目上的过期定时器不会盲目重置,所以如果一个条目变成符合刷新后没有被查询,允许被回收。
特性
统计信息
通过使用CacheBuilder.recordStats()
,你可以打开对Guava缓存的统计信息收集。Cache.stats()
方法返回一个CacheStats
对象,其提供如下统计:
hitRate()
,其返回请求命中的比率。averageLoadPenalty()
加载新值消耗的平均时间,单位纳秒。evictionCount()
缓存回收的数量。
除此之外还有更多的统计。这些统计在缓存调试中非常重要,并且我们建议在性能关键型应用程序关注这些统计。
asMap
你可以使用asMap
视图将Cache
当作ConcurrnetMap
视图,但是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
,所有key的get
调用讲迅速返回(就像其他异常一样)。而且,get
将恢复在加载线程中的中断位。出乎意料部分是InterruptedException
被封装在一个ExecutionException
。
原则上,我们可以为你拆分这个异常。但是,这导致所有的LoadingCache
用户处理InterruptedException
,虽然主要的CacheLoader
实现从没有抛出它。当你考虑到所有非加载的线程等待仍可能会被打断,也许这仍然值得这么做。尽管许多缓存仅在单线程中使用。他们的用户必须仍然捕获可能的InterruptedException
。即使跨线程间共享他们缓存的这些用户仅有时候可以中断他们的get
调,这取决于哪个线程先发出请求。
本决定中我们指导原则是让缓存的行为就像所有值都加载在调用线程中一样。这个原则使得在以前每次调用时重新计算其值的代码中引入缓存更容易。并且如果老代码是不可打断的,那么新代码也不可以。
我说过我们“在某种意义上”支持中断。在另外一种意义上我们不这么做,使loadingCache
成为一个有漏洞的抽象。如果加载线程被中断,我们会像其他异常那样对待它。在许多场景下都很好,但是当多个get
调用在等待值时,这是是不正确的。尽管发生填充值的操作被中断,其他需要值的操作并没有。所有的调用者接收InterruptedException
(封装在ExecutionException
),即使加载与其说是“失败”不如说是“中止”。正确的行为是剩余线程的一个重试加载。我们对此有一个bug存档。但是,修复可能有风险。代替修复问题,我们将额外的努力放到一个建议的AsyncLoadingCache
,其将返回具有正确中断行为的Future
对象。