最近正在重构公司平台中的一些组件,有一些涉及到缓存的处理。发现Guava的缓存还是非常不错的,所以把CachesExplained翻译了一下,供大家一起参考。
首先,看一下使用范例:
LoadingCache<Key,Graph> graphs =CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10,TimeUnit.MINUTES)
.removalListener(MY_LISTENER)
.build(
newCacheLoader<Key,Graph>(){
publicGraph load(Key key)throwsAnyException{
return createExpensiveGraph(key);
}
});
适用性
- 你愿意花费一些内存来换取性能提升;
- 你预测到某些键会多次进行查询;
-
你的缓存数据不超过内存(Guava缓存是单个应用中的本地缓存。它不会将数据存储到文件中,或者外部服务器。如果不适合你,可以考虑一下 Memcached)。
加载
From a CacheLoader
LoadingCache 缓存是通过一个CacheLoader来构建缓存。创建一个CacheLoader仅需要实现V load(K key) throws Exception方法即可。下面的范例就是如何创建一个LoadingCache:
LoadingCache<Key,Graph> graphs =CacheBuilder.newBuilder()
.maximumSize(1000)
.build(
newCacheLoader<Key,Graph>(){
publicGraph load(Key key)throwsAnyException{
return createExpensiveGraph(key);
}
});
...
try{
return graphs.get(key);
}catch(ExecutionException e){
thrownewOtherException(e.getCause());
}
通过方法get(K)可以对LoadingCache进行查询。该方法要不返回已缓存的值,要不通过CacheLoader来自动加载相应的值到缓存中。这里需要注意的是:CacheLoader可能会抛出Exception,LoaderCache.get(K)则可能会抛出ExecutionException。假如你定义的CacheLoader没有声明检查型异常,那么可以通过调用getUnchecked(K)来获取缓存值;但是一旦当CacheLoader中声明了检查型异常,则不可以调用getUnchecked。
LoadingCache<Key,Graph> graphs =CacheBuilder.newBuilder()
.expireAfterAccess(10,TimeUnit.MINUTES)
.build(
newCacheLoader<Key,Graph>(){
publicGraph load(Key key){// no checked exception
return createExpensiveGraph(key);
}
});
...
return graphs.getUnchecked(key);
批量查询可以使用getAll(Iterable<? extends K>)方法。缺省,getAll方法将循环每一个键调用CacheLoader.load方法获取缓存值。当缓存对象的批量获取比单独获取更有效时,可以通过复写CacheLoader.loadAll方法实现缓存对象的加载。此时当调用getAll(Iterable)方法时性能也会提升。
From a Callable
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,newCallable<Value>(){
@Override
publicValue call()throwsAnyException{
return doThingsTheHardWay(key);
}
});
}catch(ExecutionException e){
thrownewOtherException(e.getCause());
}
直接插入
缓存回收
基于容量回收策略
LoadingCache<Key,Graph> graphs =CacheBuilder.newBuilder()
.maximumWeight(100000)
.weigher(
newWeigher<Key,Graph>(){
publicint weigh(Key k,Graph g){
return g.vertices().size();
}
})
.build(
newCacheLoader<Key,Graph>(){
publicGraph load(Key key){// no checked exception
return createExpensiveGraph(key);
}
});
基于时间回收策略
- expireAfterAccess(long, TimeUnit) 当缓存项在指定的时间段内没有被读或写就会被回收。这种回收策略类似于基于容量回收策略;
-
expireAfterWrite(long, TimeUnit) 当缓存项在指定的时间段内没有更新就会被回收。如果我们认为缓存数据在一段时间后数据不再可用,那么可以使用该种策略。
测试定时回收
基于引用回收策略
- CacheBuilder.weakKeys() 使用弱引用存储键。当没有(强或软)引用到该键时,相应的缓存项将可以被垃圾回收。由于垃圾回收是依赖==进行判断,因此这样会导致整个缓存也会使用==来比较键的相等性,而不是使用equals();
-
CacheBuilder.weakValues() 使用弱引用存储缓存值。当没有(强或软)引用到该缓存项时,将可以被垃圾回收。由于垃圾回收是依赖==进行判断,因此这样会导致整个缓存也会使用==来比较缓存值的相等性,而不是使用equals();
-
CacheBuilder.softValues() 使用软引用存储缓存值。当响应需要时,软引用才会被垃圾回收通过最少使用原则回收掉。由于使用软引用造成性能上的影响,我们强烈建议使用可被预言的maximum cache size的策略来代替。同样使用softValues()缓存值的比较也是使用==,而不是equals()。
显示移除
- 使用Cache.invalidate(key)单个移除;
- 使用Cache.invalidteAll(keys)批量移除;
-
使用Cache.invalidateAll()移除全部。
移除监听器
CacheLoader<Key,DatabaseConnection> loader = newCacheLoader<Key,DatabaseConnection>(){
publicDatabaseConnection load(Key key)throwsException{
return openConnection(key);
}
};
RemovalListener<Key,DatabaseConnection> removalListener = newRemovalListener<Key,DatabaseConnection>(){
publicvoid onRemoval(RemovalNotification<Key,DatabaseConnection> removal){
DatabaseConnection conn = removal.getValue();
conn.close();// tear down properly
}
};
returnCacheBuilder.newBuilder()
.expireAfterWrite(2,TimeUnit.MINUTES)
.removalListener(removalListener)
.build(loader);
警告:移除监听器默认是同步进行执行的,考虑到缓存的处理也在同步执行,如果移除监听器所执行的处理非常耗时,那么会影响到缓存整体的性能。因此,如果执行一个非常耗时的移除监听器时,可以使用RemovalListeners.asynchronous(RemovalListener, Executor)将监听器包装成异步执行。
Cleanup什么时候会发生?
刷新
// 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(
newCacheLoader<Key,Graph>(){
publicGraph load(Key key){// no checked exception
return getGraphFromDatabase(key);
}
publicListenableFuture<Graph> reload(finalKey key,Graph prevGraph){
if(neverNeedsRefresh(key)){
returnFutures.immediateFuture(prevGraph);
}else{
// asynchronous!
ListenableFutureTask<Graph> task =ListenableFutureTask.create(newCallable<Graph>(){
publicGraph call(){
return getGraphFromDatabase(key);
}
});
executor.execute(task);
return task;
}
}
});
通过CacheBuilder.refreshAfterWrite(long, TimeUnit)方法可以实现缓存的自动刷新。相对expiredAfterWrite来说,refreshAfterWrite方法可以让键在一段时间后进行刷新,但是需要注意的是,只有当该条目被访问后才会真正的去刷新(加入CacheLoader.refresh实现为异步的方式,那么你不用担心刷新会影响缓存查询的性能)。因此,你可以在缓存中同时实现refreshAfterWrite和expireAfterWrite,此时过期定时器是不会在缓存项刷新后盲目的重置,也就是说当一个缓存项在刷新后没有被访问过,那么它是允许被过期的。
其它特性
统计
- hitRate(),提供了请求命中率;
- averageLoadZPenalty(),加载平均耗时,单位为纳秒;
-
evictionCount(),缓存移除总数
asMap
- cache.asMap()包含了当前缓存中所加载的所有条目。也就是说,cache.asMap().keySet包含了当前缓存中已经加载的键。
- asMap().get(key)等同于cache.getIfPresent(key),不会调用到加载方法。这一点和Map的语义是一致的。
-
缓存的读写操作(包含了Cache.asMap().get(Object)和Cache.asMap().put(K,V))都会重置缓存的访问时间,但containsKey(Object)方法不会重置访问时间,Cache.asMap()方法也不会。因此,通过cache.entrySet()迭代访问是不会重置缓存条目的访问时间。