转载

Java 类库中的瑞士军刀:Google Guava 缓存

Java 类库中的瑞士军刀:Google Guava 缓存

Google Guava 被誉为是JAVA类库中的瑞士军刀。能显著简化代码,让代码易写、易读、易于维护。同时可以大幅提高程序员的工作效率,让我们从大量重复的底层代码中脱身。

由于 Google Guava 类库包含大量非常有用的特性,无法在一篇文章中尽述。本篇仅简单介绍 Google Guava 中的缓存工具的使用。

依赖

使用 Maven 进行项目构建时,添加下面的依赖:

<dependency> 
 
<groupId>com.google.guava</groupId> 
 
<artifactId>guava</artifactId> 
 
<version>29.0-jre</version> 
 
<!-- or, for Android: --> 
 
<version>29.0-android</version> 
 
</dependency> 

使用 Gradle 进行项目构建时,添加下面的依赖:

dependencies { 
 
// Pick one: 
 
// 1. Use Guava in your implementation only: 
 
implementation("com.google.guava:guava:29.0-jre") 
 
// 2. Use Guava types in your public API: 
 
api("com.google.guava:guava:29.0-jre") 
 
// 3. Android - Use Guava in your implementation only: 
 
implementation("com.google.guava:guava:29.0-android") 
 
// 4. Android - Use Guava types in your public API: 
 
api("com.google.guava:guava:29.0-android") 
 
} 

示例

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

适用性

缓存有非常广泛的应用场景。比如,你应该为那些计算或者查询代价高昂的数据使用缓存,或者你需要某个输入数据很多次的场景。

一个 `Cache` 类似于 `ConcurrentMap`,不过并不完全相同。基本的差异在于, `ConcurrentMap` 持久化所有添加进来的元素直到它们被显式删除。另一方面,通常将 `Cache` 配置为自动淘汰条目,以限制其内存占用量。在某些情况下, `LoadingCache` 会很有用,虽然它不淘汰条目,但是可以自动加载缓存。

通常,Guava 缓存工具可以适用于下列场景:

  • 你希望使用一些内存空间来改善速度。
  • 您希望多次查询某些键。
  • 您的缓存将不需要存储超出 RAM 容量的数据。(Guava 缓存的作用范围局限于在应用程序的一次运行中。它们不将数据存储在文件中或外部服务器上。如果这不符合您的需求,请考虑使 Memcached)

如果这些都适用于您的应用场景,那么 Guava 缓存实用程序将很适合您!

如上面的示例代码所示,使用 `CacheBuilder` 生成器模式可以获取 `Cache`,但是自定义缓存是有趣的部分。

注意:如果不需要 `Cache` 的功能,则 `ConcurrentHashMap` 的内存使用效率更高——但是很难用任何旧的 `ConcurrentMap`来复制大多数 `Cache` 的功能。

填充

你需要问自己有关缓存的第一个问题是:是否有一些合理的默认函数来加载或计算与键关联的值?如果是这样,您应该使用 `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`。(如果缓存加载器抛出 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` 可能会同时加载其余组。

使用 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()); 
 
} 

直接插入

可以直接使用 `cache.put(key, value)` 。这将覆盖高速缓存中指定键的任何先前条目。也可以使用 `Cache.asMap()` 视图公开的任何 `ConcurrentMap` 方法对缓存进行更改。注意,`asMap` 视图上的任何方法都不会导致条目自动加载到缓存中。此外,该视图上的原子操作在自动缓存加载范围之外运行,因此在使用 `CacheLoader` 或 `Callable` 加载值的缓存中,始终应优先选择 `Cache.get(K, Callable<V>)` 而不是 `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)` 自创建条目以来经过指定的时间或该值的最新替换之后,使条目过期。如果经过一定时间后缓存的数据持续增长,则可能需要这样做。

定时到期是在写入过程中进行定期维护的,偶尔在读取过程中进行维护,如下所述。

基于引用的驱逐

Guava 允许你设置你的缓存以允许数据实体的垃圾收集,通过对键或者值使用的 weak references ,或者对值使用的 soft references 进行设置。

  • `CacheBuilder.weakKeys()` 使用弱引用存储键。这允许实体在没有其他引用(强引用或者软引用)指向其键时被垃圾收集。由于垃圾收集基于 id 相等规则,这就导致整个缓存多需要使用 id (`==`)相等来比较键,而不是使用 `equals()`。
  • `CacheBuilder.weakValues()` 使用弱引用存储值。这允许实体在没有其他引用(强引用或者软引用)指向其值时被垃圾收集。由于垃圾收集基于 id 相等规则,这就导致整个缓存多需要使用 id (`==`)相等来比较值,而不是使用 `equals()`。
  • `CacheBuilder.softValues()` 将值包装进入软引用。软引用对象以全局最近最少使用规则进行垃圾收集,以响应内存需求。由于使用软引用可能会有些性能问题,我们通常推荐使用更加容易预测的 maximum cache size 替代。使用 `softValues()` 将导致值被通过 id (`==`) 相等比较,而不是使用 `equals()`。

显式删除

任何时刻,你都可以显式废除缓存实体,而不需要等待实体被驱逐。可以通过以下方法:

  • 单个废除,使用 `Cache.invalidate(key)`
  • 批量废除,使用 `Cache.invalidateAll(keys)`
  • 全部废除,使用 `Cache.invalidateAll()`

清理何时发生?

用 `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` 相比,`refreshAfterWrite` 在指定的持续时间后将使键“具有资格”进行刷新,但实际上仅在查询条目时才会启动刷新。(如果将 `CacheLoader.reload` 实现为异步,则刷新不会降低查询的速度。)因此,例如,您可以在同一缓存上同时指定 `refreshAfterWrite` 和 `expireAfterWrite`,以便只要条目符合刷新资格,就不会盲目地重置条目的过期计时器,因此,如果在符合刷新资格后不查询条目,则允许它过期。

原文  http://news.51cto.com/art/202007/620090.htm
正文到此结束
Loading...