• Guava Cache 总结


    想对Guava cache部分进行总结,但思索之后,文档才是最全面、详细的。所以,决定对guava文档进行翻译。

    英文地址如下:https://github.com/google/guava/wiki/CachesExplained

    花费了一些时间进行翻译,翻译的水平有待提高,有些地方翻译的不准确,因为有些没有实际用到,所以无法给出清晰的解释。

    如果对您有帮助,莫感欣慰!!!

    一 概要

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

    二内存解释

    Example

     1 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
     2        .maximumSize(1000)
     3        .expireAfterWrite(10, TimeUnit.MINUTES)
     4        .removalListener(MY_LISTENER)
     5        .build(
     6            new CacheLoader<Key, Graph>() {
     7              public Graph load(Key key) throws AnyException {
     8                return createExpensiveGraph(key);
     9              }
    10            });

    应用性

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

     1 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
     2        .maximumSize(1000)
     3        .build(
     4            new CacheLoader<Key, Graph>() {
     5              public Graph load(Key key) throws AnyException {
     6                return createExpensiveGraph(key);
     7              }
     8            });
     9 
    10 ...
    11 try {
    12   return graphs.get(key);
    13 } catch (ExecutionException e) {
    14   throw new OtherException(e.getCause());
    15 }

    查询LoadingCache 的权威方法是用get(K) 。如果已经换存了值,就会直接返回;如果没有,就会使用CacheLoader 来往缓存中自动导入一个新值。因为CacheLoader 会抛出Exception ,LoadingCache.get(K)可能会抛出ExecutionException 。你也可以用getUnchecked(K) ,它在UncheckedExecutionException 中包装了所有的UncheckedExecutionException ,但是,如果CacheLoader 抛出了 checked exceptions的话,会导致奇怪的行为发生。

     1 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
     2        .expireAfterAccess(10, TimeUnit.MINUTES)
     3        .build(
     4            new CacheLoader<Key, Graph>() {
     5              public Graph load(Key key) { // no checked exception
     6                return createExpensiveGraph(key);
     7              }
     8            });
     9 
    10 ...
    11 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() 使用,对内存的修改才有了一个可观察的状态。这个方法为“如果缓存了,返回缓存之;没有缓存则创建,缓存并放回”这个模式提供了一个简单的替代品。

     1 Cache<Key, Value> cache = CacheBuilder.newBuilder()
     2     .maximumSize(1000)
     3     .build(); // look Ma, no CacheLoader
     4 ...
     5 try {
     6   // If the key wasn't in the "easy to compute" group, we need to
     7   // do things the hard way.
     8   cache.get(key, new Callable<Value>() {
     9     @Override
    10     public Value call() throws AnyException {
    11       return doThingsTheHardWay(key);
    12     }
    13   });
    14 } catch (ExecutionException e) {
    15   throw new OtherException(e.getCause());
    16 }

    直接插入
    值必须用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 所要求的,要意识到权重时每回创建时计算的,并且,那之后,是静态的。

     1 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
     2        .maximumWeight(100000)
     3        .weigher(new Weigher<Key, Graph>() {
     4           public int weigh(Key k, Graph g) {
     5             return g.vertices().size();
     6           }
     7         })
     8        .build(
     9            new CacheLoader<Key, Graph>() {
    10              public Graph load(Key key) { // no checked exception
    11                return createExpensiveGraph(key);
    12              }
    13            });

    超时驱逐
    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() 只要引用存在,垃圾回收器永远不会回收掉被引用的对象
    软引用:描述一些有用,但非必须的对象。在系统将要发生内存溢出时,会将这些对象放进回收范围之内,进行回收
    弱引用:描述非必需的对象,强度比软引用弱,无论当前内存是否充足,垃圾回收时都会对其进行回收
    虚医用:最弱的一种引用关系。设置虚引用,唯一的目的就是,在这个对象呗收集器回收时收到一个系统通知
    引自《深入理解Java虚拟机-周志华 )


    ・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里。

     1 CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
     2   public DatabaseConnection load(Key key) throws Exception {
     3     return openConnection(key);
     4   }
     5 };
     6 RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
     7   public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
     8     DatabaseConnection conn = removal.getValue();
     9     conn.close(); // tear down properly
    10   }
    11 };
    12 
    13 return CacheBuilder.newBuilder()
    14   .expireAfterWrite(2, TimeUnit.MINUTES)
    15   .removalListener(removalListener)
    16   .build(loader);


    警告:监视器的操作默认是同步的,因此,内存的保持一般来说都是正常操作。花费(时间)较大的监视器会拖慢缓存的功能。如果,你有一个花费(时间)较大的监视器,异步地使用RemovalListeners.asynchronous(RemovalListener, Executor) 来装饰RemovalListener 。


    什么时候发生清空操作?
    用CacheBuilder 建立的缓存不会发生清空,不会自动驱逐值,不会当值过期后立即清除,不会清除任何排序的东西。相反,在读写操作发生后,它会有短暂的保留。


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


    刷新
    刷新和驱逐不太一样。正如LoadingCache.refresh(K) 中指定的,刷新key导入一个新值,可能是异步地操作。和驱逐做对比,当刷新时,强制查询直到获取新值时,返回的仍是旧值。
    如果刷新时有异常发生,异常会被记录在log中。
    CacheLoader 会以通过覆盖CacheLoader.reload(K, V) 这个方法来使用刷新。这个方法允许你在计算新值时使用旧值。

     1 // Some keys don't need refreshing, and we want refreshes to be done asynchronously.
     2 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
     3        .maximumSize(1000)
     4        .refreshAfterWrite(1, TimeUnit.MINUTES)
     5        .build(
     6            new CacheLoader<Key, Graph>() {
     7              public Graph load(Key key) { // no checked exception
     8                return getGraphFromDatabase(key);
     9              }
    10 
    11              public ListenableFuture<Graph> reload(final Key key, Graph prevGraph) {
    12                if (neverNeedsRefresh(key)) {
    13                  return Futures.immediateFuture(prevGraph);
    14                } else {
    15                  // asynchronous!
    16                  ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() {
    17                    public Graph call() {
    18                      return getGraphFromDatabase(key);
    19                    }
    20                  });
    21                  executor.execute(task);
    22                  return task;
    23                }
    24              }
    25            });


    使用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 对象。

  • 相关阅读:
    【转】fastjson-1.2.47-RCE
    某安全设备未授权访问+任意文件下载0day
    关于伴侣
    【转】Why BIOS loads MBR into 0x7C00 in x86 ?
    【生活】北京旅游攻略
    利用Python读取图片exif敏感信息
    A MacFUSE-Based Process File System for Mac OS X
    linux-强制断开远程tcp连接
    Navicat use HTTP Tunnel
    python mac下使用多进程报错解决办法
  • 原文地址:https://www.cnblogs.com/lihao007/p/9530496.html
Copyright © 2020-2023  润新知