本地缓存Guava Cache工具的介绍

本地缓存Guava Cache工具的介绍


原文地址: github wiki

Example

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);
             }
           });

Applicability 适用范围

缓存在各种用例中都非常有用。 例如,当一个值的计算或检索成本很高时,并且您将多次在某个输入上需要它的值,您应该考虑使用缓存。

Cache 类似于 ConcurrentMap,但并不完全相同。 最根本的区别是,ConcurrentMap 会保留所有添加到其中的元素,直到它们被显式删除。 另一方面,Cache 通常配置为自动驱逐条目,以限制其内存占用。 在某些情况下,LoadingCache 即使它不驱逐条目也会很有用,因为它会自动加载缓存。

通常,Guava 缓存实用程序适用于以下情况:

  • 你愿意花一些内存来提高速度。
  • 您希望键有时会被多次查询
  • 您的缓存不需要存储比 RAM 容量更多的数据。(Guava 缓存对于您的应用程序的单次运行是本地的。它们不会将数据存储在文件中或外部服务器上。如果这不符合您的需求,请考虑使用类似 Memcached.)

如果这些都适用于您的用例,那么 Guava 缓存实用程序可能适合您!

如上面的示例代码所示,使用CacheBuilder构建器模式来获取Cache,但自定义缓存是有趣的部分。

注意: 如果你不需要 Cache 的特性,ConcurrentHashMap 更节省内存——但是用任何旧的 ConcurrentMap 复制大多数 Cache 特性是极其困难或不可能的。

关于缓存的第一个问题是:是否有一些合理的默认函数来加载或计算与key关联的value? 如果是这样,您应该使用 CacheLoader。 如果没有,或者如果您需要覆盖默认值,但您仍然需要原子的“get-if-absent-compute”语义,则应该将 Callable 传递给 get 调用。 可以使用 Cache.put 直接插入元素,但首选自动缓存加载,因为它可以更容易地推断所有缓存内容的一致性。

“get-if-absent-compute”语义:进行查询,若存在则返回,不存在则创建新值返回

Population 构建

From a CacheLoader 通过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。 (如果缓存加载器抛出 unchecked 异常,get(K) 将抛出 UncheckedExecutionException 包装它。)您也可以选择使用 getUnchecked(K),它将所有异常包装在 UncheckedExecutionException 中, 但是如果底层的 CacheLoader 通常会抛出检查异常,这可能会导致令人惊讶的行为。

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 的单独调用。 当批量检索比许多单独查找更有效时,您可以覆盖 CacheLoader.loadAll 来利用它。 getAll(Iterable) 的性能会相应提高。

请注意,您可以编写一个 CacheLoader.loadAll 实现来加载未特别请求的键的值。 例如,如果计算某个组中任何键的值,可以为您提供组中所有键的值,则 loadAll 可能会同时加载组中的其余部分。

From a Callable 通过Callable

所有 Guava 缓存,无论是否加载,都支持方法 get(K, Callable)。 此方法返回与缓存中的键关联的值,或从指定的 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, new Callable<Value>() {
    @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 视图上的任何方法都不会导致条目自动加载到缓存中。 此外,该视图上的原子操作在自动缓存加载范围之外运行,因此Cache.get(K, Callable<V>)应始终优先于缓存中的Cache.asMap().putIfAbsent()使用 CacheLoaderCallable 加载值。 请注意,Cache.get(K, Callable) 也可以将值插入到底层缓存中。

Eviction 清除

残酷的现实是,我们几乎肯定没有足够的内存来缓存我们可以缓存的所有内容。 您必须决定:什么时候不值得保留缓存条目? Guava 提供了三种基本的驱逐类型:基于大小的驱逐、基于时间的驱逐和基于引用的驱逐。

Size-based Eviction 按记录数量进行清除

如果您的缓存不应超过一定大小,只需使用 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);
             }
           });

Timed Eviction 按时间进行清除

CacheBuilder 提供了两种定时驱逐的方法:

定时到期是在写入期间和偶尔在读取期间定期维护的情况下执行的,如下所述。

Testing Timed Eviction 测试按时间清除策略

测试定时驱逐不一定是痛苦的…实际上并不需要花两秒钟来测试两秒钟的到期时间。 使用 Ticker 接口和 CacheBuilder.ticker(Ticker) 方法在缓存构建器中指定时间源, 而不必等待系统时钟。

Reference-based Eviction 基于引用进行清除

Guava 允许您设置缓存以允许对条目进行垃圾回收,方法是使用 弱引用 用于键或值,并使用 软引用 用于值。

  • CacheBuilder.weakKeys() 使用弱引用存储键。 如果没有对键的其他(强或软)引用,这允许对条目进行垃圾收集。 由于垃圾收集仅依赖于身份相等,这会导致整个缓存使用身份(==)相等来比较键,而不是equals()
  • CacheBuilder.weakValues() 使用弱引用存储值。 如果没有对值的其他(强或软)引用,这允许对条目进行垃圾收集。 由于垃圾回收仅依赖于身份相等,这会导致整个缓存使用身份(==)相等来比较值,而不是equals()
  • CacheBuilder.softValues() 将值包装在软引用中。 软引用的对象以全最近最少使用的方式进行垃圾收集,以响应内存需求。 由于使用软引用对性能的影响,我们通常建议使用更可预测的最大缓存大小代替。 使用 softValues() 将导致使用身份 (==) 相等性而不是 equals() 来比较值。

Explicit Removals 主动的清除

在任何时候,您都可以显式地使缓存条目无效,而不是等待条目被驱逐。 可以这样做:

Removal Listeners 删除监听器

您可以通过CacheBuilder.removalListener(RemovalListener)为缓存指定一个删除侦听器,以便在删除条目时执行某些操作。 RemovalListener 被传递了一个 RemovalNotification,它指定了RemovalNotification、key和value。

请注意,任何由 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 以异步操作。

When Does Cleanup Happen?

使用 CacheBuilder 构建的缓存不会“automatically”或在值过期后立即执行清理和驱逐值,或任何类似的操作。相反,它在写入操作期间执行少量维护,或者如果写入很少,则在偶尔的读取操作期间执行。

原因如下:如果我们想要持续地进行Cache维护,我们需要创建一个线程,它的操作会和用户操作竞争共享锁。此外,某些环境会限制线程的创建,这会使 CacheBuilder 在该环境中无法使用。

相反,我们将选择权交给您。如果您的缓存是高吞吐量的,那么您不必担心执行缓存维护以清理过期条目等。如果您的缓存很少写入并且您不希望清理以阻止缓存读取,您可能希望创建自己的维护线程来调用 Cache.cleanUp() 定期。

如果您想为很少有写入的缓存安排定期缓存维护,只需使用 ScheduledExecutorService

Refresh

刷新与驱逐并不完全相同。 如 LoadingCache.refresh(K) 中指定,刷新一个 key 为键加载一个新值,可能是异步的。 刷新键时仍会返回旧值(如果有),这与驱逐相反,驱逐强制检索等到重新加载值。

如果刷新时抛出异常,则保留旧值,并记录并吞下异常。

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 将使键在指定的持续时间后有资格刷新,但只有在查询条目时才会真正启动刷新。 (如果 CacheLoader.reload 被实现为异步,则查询不会因刷新而变慢。)因此,例如,您可以在同一个缓存上同时指定 refreshAfterWriteexpireAfterWrite,这样 每当条目符合刷新条件时,条目上的过期计时器不会盲目重置,因此如果条目在符合刷新条件后未被查询,则允许它过期。

Features 特性

Statistics 统计

通过使用CacheBuilder.recordStats(),可以开启统计 Guava 缓存的集合。 Cache.stats() 方法返回一个 CacheStats 对象,它提供了诸如

  • hitRate(), which returns the ratio of hits to requests 它返回命中与请求的比率
  • averageLoadPenalty(), the average time spent loading new values, in nanoseconds 加载新值所花费的平均时间,以纳秒为单位
  • evictionCount(), the number of cache evictions 缓存驱逐的数量

还有更多的统计数据。 这些统计数据在缓存调整中至关重要,我们建议在性能关键型应用程序中密切关注这些统计数据。

asMap map视图

You can view any Cache as a ConcurrentMap using its asMap view, but how the asMap view interacts with the Cache requires some explanation.

您可以使用其 asMap 视图将任何 Cache 视为 ConcurrentMap,但 asMap 视图如何与 Cache 交互需要一些解释。

  • cache.asMap() contains all entries that are currently loaded in the cache. So, for example, cache.asMap().keySet() contains all the currently loaded keys.包含缓存中当前加载的所有条目。 因此,例如,cache.asMap().keySet() 包含所有当前加载的键。
  • asMap().get(key) is essentially equivalent to cache.getIfPresent(key), and never causes values to be loaded. This is consistent with the Map contract.本质上等同于 cache.getIfPresent(key),并且永远不会导致加载值。 这与 Map 合约一致。
  • Access time is reset by all cache read and write operations (including Cache.asMap().get(Object) and Cache.asMap().put(K, V)), but not by containsKey(Object), nor by operations on the collection-views of Cache.asMap(). So, for example, iterating through cache.asMap().entrySet() does not reset access time for the entries you retrieve.访问时间由所有缓存读写操作(包括 Cache.asMap().get(Object)Cache.asMap().put(K, V))重置,但不通过 containsKey(Object),也不是通过对 Cache.asMap() 的集合视图的操作。 因此,例如,遍历 cache.asMap().entrySet() 不会重置您检索的条目的访问时间。

Interruption

加载方法(如 get)永远不会抛出 InterruptedException。我们本可以设计这些方法来支持InterruptedException,但我们的支持是不完整的,这迫使所有用户承担成本,而只对部分用户产生好处。有关详细信息,请继续阅读。

请求未缓存值的get调用分为两大类:加载值的调用和等待另一个线程正在进行的加载的调用。两者在我们支持中断的能力上有所不同。最简单的情况是等待另一个线程正在进行的负载:这里我们可以进入一个可中断的等待。困难的情况是我们自己加载value。在这里,我们受制于用户提供的 CacheLoader。如果恰好支持中断,我们可以支持中断;如果没有,我们不能。

那么为什么在提供的 CacheLoader 不支持中断呢? 从某种意义上说,我们这样做了(但见下文):如果 CacheLoader 抛出 InterruptedException,则所有对 key 的 get 调用都会立即返回(就像任何其他异常一样)。 另外,get 将在恢复加载线程中的中断位。 令人惊讶的是,InterruptedException 被包裹在ExecutionException 中。 ???

原则上,我们可以为您解开此异常。 但是,这会强制所有 LoadingCache 用户处理 InterruptedException,即使大多数 CacheLoader 实现从未抛出它。 当您考虑到所有非加载线程的等待仍可能被中断时,也许这仍然值得。 但是许多缓存仅在单个线程中使用。 他们的用户仍然必须捕获不可能的 InterruptedException。 甚至那些跨线程共享缓存的用户也只能有时中断他们的get调用,这取决于哪个线程恰好首先发出请求。

我们在这个选择中的指导原则是让缓存表现得好像所有值都加载到调用线程中一样。 这个原则可以很容易地将缓存引入到以前在每次调用时重新计算其值的代码中。 如果旧代码不可中断,那么新代码也可以不中断。

我说我们“在某种意义上”支持打断。 在另一种意义上我们没有这样做,使 LoadingCache 成为一个泄漏的抽象。 如果加载线程被中断,我们将其视为任何其他异常。 在许多情况下这很好,但当多个 get 调用正在等待该值时,这不是正确的事情。 尽管恰好计算该值的操作被中断,但其他需要该值的操作可能没有被中断。 然而,所有这些调用者都会收到 InterruptedException(包装在 ExecutionException 中),即使加载并没有像“中止”那样“失败”。 正确的行为是让剩余线程之一重试加载。 我们有 为此提交的错误。 但是,修复可能是有风险的。 除了解决问题,我们可能会在提议的 AsyncLoadingCache 中投入额外的精力,这将返回具有正确中断行为的 Future 对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

顧棟

若对你有帮助,望对作者鼓励一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值