GUAVA CACHE(缓存) 总结

Posted Java经验总结

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GUAVA CACHE(缓存) 总结相关的知识,希望对你有一定的参考价值。

精品技术文章准时送上!


转载:http://1t.click/aNz7

前言
想对Guava cache部分进行总结,但思索之后,文档才是最全面、详细的。 所以,决定对guava文档进行翻译。
英文地址如下: https://github.com/google/guava/wiki/CachesExplained
花费了一些时间进行翻译,翻译的水平有待提高,有些地方翻译的不准确,因为有些没有实际用到,所以无法给出清晰的解释。

一、概要
Guava cache是google开发的,目前被常用在单机上,如果是分布式,它就无能为力了。 废话不多说,下面开始进入正文。
二、内存解释

Example

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .removalListener(MY_LISTENER) .build( new CacheLoader<Key, Graph>() { public Graph load(Key key) throws AnyException { return createExpensiveGraph(key); } });

应用性

缓存在许多的地方非常的有用。 比如: 当一个计算或是查询一个值花费很大代价时,或者,你需要多次用到一个值时,你应该考虑使用缓存。

Cache 跟ConcurrentMap 很像,但不一样。最大的功能上的区別,ConcurrentMap允许所有的元素直到被手动移除为止,一直存在。另一方面,Cache为了限制内存的占用,通常会自动地移除值。某些时候,LoadingCache 即使不驱除元素,但由于他自动导入缓存的特点,它也是十分有用的。

一般情况下,当满足以下场景时:

  • 希望花费一下内存来提高速度

  • 有些keys会被多次查询

  • 你的cache保存的东西不会超过你机器的内存量
此时,你应该选择Guava cache
获得一个Cache 用上面的code例子就可以了,但是自定义一个cache 会更有趣。

渲染

问自己关于你的内存的第一个问题应该是: 有什么默认的方法来导入或是计算key的值吗。 如果是这样的话,你应使用CacheLoader 。 如果不是的话,或者说,你需要覆盖掉默认的方法,但你仍想保留“存在就直接获取,不存在就去计算”这种机制时,你应该往get方法调用中传一个callable 。 使用Cache.put 可以直接插入元素,但是从所有数据缓存一致性方面来说,使用自动的缓存导入方法更加简单。

使用CacheLoader

一个LoadingCache 就是关联了一个CacheLoader 的缓存。 创建一个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 。 你也可以用getUnchecked(K) ,它在UncheckedExecutionException 中包装了所有的UncheckedExecutionException ,但是,如果CacheLoader 抛出了 checked exceptions的话,会导致奇怪的行为发生。
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 会对CacheLoader.load 产生一个单独的调用,对cache中每个不存在缓存值的key ,进行取值。当体积的查询已经比单个查询效率更高时,你可以通过覆盖CacheLoader.loadAll 方法,来开发它。
注意: 你可以写一个CacheLoader.loadAll 的实现为那些没有特殊指定的key来导入值。 比如: 如果计算某些group中的任意key的值,会给你group内所有key的值,loadAll 也许会同时导入group内其他key的值。

From a Callable

所有Guava缓存,无论是否是导入,都支持get(K, Callable<V>) 方法。 这个方法返回内存中这个key关联的值,或是用Callable 接口计算得到的值并将它加入内存中。 知道load() 使用,对内存的修改才有了一个可观察的状态。 这个方法为“如果缓存了,返回缓存之; 没有缓存则创建,缓存并放回”这个模式提供了一个简单的替代品。
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 视图中没有任何方法会让键值对自动导入到内存中,所以使用Cache.get(K, Callable<V>) 与使用CacheLoader 或是 Callable 来导入内存的Cache.asMap().putIfAbsent相比,前者更好。

 

驱逐

残酷的事实是我们没有足够的内存缓存所有东西。 你必须决定: 何时内存值不值得保存了。 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) 当被创建的键值对或是最近被替换过的,经过一段存活期间后,会走向死亡。这个可用于经历过一段期间后,缓存的数据变得过期数据,这样场景下使用。

Testing Timed Eviction

测试超时驱逐不是很难,也不必花上2秒钟去测试一个2秒超时。使用Ticker 接口和 CacheBuilder.ticker(Ticker) 方法在你的cache 中指定时间,而不是去等待系统时钟的2秒。


基于引用的驱逐

Guava 允许你建立基于垃圾回收的缓存,可以使用弱引用和软引用。
(注:Java中的引用分为四种:强、软、弱、虚

强引用:Java之中普遍存在,如Object object = new Object() 只要引用存在,垃圾回收器永远不会回收掉被引用的对象

软引用:描述一些有用,但非必须的对象。在系统将要发生内存溢出时,会将这些对象放进回收范围之内,进行回收

弱引用:描述非必需的对象,强度比软引用弱,无论当前内存是否充足,垃圾回收时都会对其进行回收

虚引用:最弱的一种引用关系。设置虚引用,唯一的目的就是,在这个对象呗收集器回收时收到一个系统通知

  • CacheBuilder.weakKeys() 使用弱引用来保存key值。如果没有其他引用指向这个key,那么它将允许被垃圾收集器回收掉。既然垃圾回收仅依赖于恒等式的一致,这就导致整个缓存用 == 来比较key,而不是equals().

  • CacheBuilder.weakValues() 使用弱引用来保存value值。如果没有其他引用指向这个value,那么它将允许被垃圾收集器回收掉。既然垃圾回收仅依赖于恒等式的一致,这就导致整个缓存用 == 来比较value,而不是equals()

  • CacheBuilder.softValues() 用软引用包装值。应对内存的需求,软引对象使用最近最少使用条例,来进行垃圾回收。因为使用软引用的性能上的关系,我们通常建议使用最大缓存数量。softValues() 的使用会导致使用整个缓存用 == 比较value,而不是equals()。


监视移除

你会制定一个监视器,可以通过CacheBuilder.removalListener(RemovalListener) ,来监视键值对在缓存中被移除。 RemovalListener 获得了一个RemovalNotification, 它指定了RemovalCause ,键和值。
注意,任何被RemovalListener 抛出的异常都会被打进log里。
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 。

什么时候发生清空操作?

用CacheBuilder 建立的缓存不会发生清空,不会自动驱逐值,不会当值过期后立即清除,不会清除任何排序的东西。 相反,在读写操作发生后,它会有短暂的保留。
原因如下: 如果要缓存一直可用,那么我们需要创建一个线程,它的操作需要user的操作来完成。 此外,一些环境限制我们创建线程,这样,会导致CacheBuilder 不可用。
相反呢,我们让您来决定。 如果缓存有比较高的吞吐量,那么你不必担心缓存一直可用会清理掉过期的键值对。 如果你的缓存,仅仅的写操作,你不想让清空来锁住缓存的读取,你会希望创建你自己的保持线程,以常规的间隔来调用Cache.cleanUp() 。
如果你想为几乎只有写操作的缓存来定制常规的内存保持,那么就用ScheduledExecutorService 。

刷新

刷新和驱逐不太一样。 正如LoadingCache.refresh(K) 中指定的,刷新key导入一个新值,可能是异步地操作。 和驱逐做对比,当刷新时,强制查询直到获取新值时,返回的仍是旧值。
如果刷新时有异常发生,异常会被记录在log中。
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 相比较,refreshAfterWrite 会让一个值在指定的时间段之后进行刷新,但是刷新也只有当键值对被查询时才会开始。 所以,举例子来说,你可以同时指定refreshAfterWrite 和 expireAfterWrite ,所以当键值对可以被刷新时,驱逐计时器不会盲目地被重置,所以,当一个键值对可以被刷新时,但是此时没有被查询,那么,它将会被驱逐。

三、特性

统计数据

通过CacheBuilder.recordStats() 你可以为Guava 缓存打开数据收集。 Cache.stats() 方法返回一个Cache.stats() 对象,这个对象提供了统计数据,如:
  • hitRate() 返回采样数的比率

  • averageLoadPenalty() 导入新值平均花费时长 单位:纳秒

  • evictionCount() 缓存驱逐的个数

此外还有许多其他的统计数据。 这些统计数据在缓存优化方面启动决定性作用,我们建议在性能很重要的应用中,留心这些统计数据。

asMap

你可以将缓存看做是一个使用asMap 视图的ConcurrentMap 。 但是,asMap 视图和缓存如何交互需要下面的一些解释。
  • cache.asMap() 包含了所有现在导入缓存中的键值对。所以,比如,cache.asMap().keySet() 包含了所有导入的key。

  • asMap().get(key) 本质上与cache.getIfPresent(key) 相等,从不会引起值的导入。这个和Map相比,是一致的。

  • 读写操作会导致access time被重置。但containsKey(Object) 和Cache.asMap() 操作不会导致重置发生。举例子来说,用cache.asMap().entrySet() 来迭代不会导致access time被重置。

中断

像get() 这样的导入方法永远不会抛出InterruptedException。 不过,我们可以设计这些方法来支持InterruptedException 。 但是,我们的支持并不是完整的,强制地在所用用户上产生花销只会收益很少。 具体来说,比如读取。
get 把那些请求的、未缓存的值大体分为两类: 那些导入的的值和那些等待另一个线程导入的值。 这两者以不同方式支持中断。 简单的方法是等待另一个正在执行的线程完事后,再进行导入。 这里呢,我们就会进入可中断的等待。 比较难的方法是我们自己导入值。 我们用用户定义的CacheLoader 。 如果它支持中断,那么我们可以支持中断; 如果不行,那么我们也不能支持中断。
那么为什么当提供的CacheLoader 支持中断,而自定义的不支持呢? 某种意义上来说,我们支持中断。 如果CacheLoader 抛出中断异常,所有关于key 的调用会立即返回。 此外,get 会在导入线程中存储中断标记位。 惊奇的是,InterruptedException 被包装在ExecutionException 中。
原则上讲,我们可以不为你包装这个异常。 然而,这将导致强迫所有LoadingCache 用户去处理InterruptedException ,即使是那些从未抛出中断异常的、CacheLoader 的实现。 也许你考虑那些非导入线程的登台可以诶中断是值得的,但是需要缓存只是单一线程。 他们额用户必须仍要catch不可能的InterruptedException 。
在这部分我们的原则是让缓存在所有调用的线程中导入值。 这个原则让每个调用中再计算值变得简单。 如果旧代码不可被中断,那么,或许对于新代码来说也是不可被中断。
我说过我们在某种意义上支持中断。 在另一层(让LoadingCache 作为有漏洞的抽象)来说,我们不支持中断。 如果导入线程被中断了,我们很可能将这个异常看做其他异常。 这个,在很多地方来说,没有大碍。 但是当多次调用get 等待返回值时,就会出错。 虽然,刚巧要计算值得操作被中断了,其他的需要这个值的一些操作不会被执行。 然而,这些调用者收到InterruptedException (包装在ExecutionException中), 即使导入没有将失败作为终止。 正确的行为将是遗留下来的一个线程再次进行尝试。 关于我们有个一个bug列表(https://github.com/google/guava/issues/1122)。 然而,修正的话也有一定风险。 并非是修正问题,我们会投入额外的精力到被推荐的AsyncLoadingCache 中,它面对中断会做出正确的行为,同时返回Future 对象。

GUAVA CACHE(缓存) 总结

(完)GUAVA CACHE(缓存) 总结

GUAVA CACHE(缓存) 总结
GUAVA CACHE(缓存) 总结

专注于Java干货享

扫描二维码

获取Java干货

点个在看少个bug

以上是关于GUAVA CACHE(缓存) 总结的主要内容,如果未能解决你的问题,请参考以下文章

使用google guava做内存缓存

Guava Cache源码详解

Guava Cache 缓存工具使用

Guava Cache 缓存工具使用

Guava Cache本地缓存

分布式缓存系列之guava cache