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

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

[[332051]]

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

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

依赖

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

  1. <dependency> 
  2.  
  3. <groupId>com.google.guava</groupId> 
  4.  
  5. <artifactId>guava</artifactId> 
  6.  
  7. <version>29.0-jre</version> 
  8.  
  9. <!-- or, for Android: --> 
  10.  
  11. <version>29.0-android</version> 
  12.  
  13. </dependency> 

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

  1. dependencies { 
  2.  
  3. // Pick one: 
  4.  
  5. // 1. Use Guava in your implementation only: 
  6.  
  7. implementation("com.google.guava:guava:29.0-jre"
  8.  
  9. // 2. Use Guava types in your public API: 
  10.  
  11. api("com.google.guava:guava:29.0-jre"
  12.  
  13. // 3. Android - Use Guava in your implementation only: 
  14.  
  15. implementation("com.google.guava:guava:29.0-android"
  16.  
  17. // 4. Android - Use Guava types in your public API: 
  18.  
  19. api("com.google.guava:guava:29.0-android"
  20.  

示例

  1. LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() 
  2.  
  3. .maximumSize(1000
  4.  
  5. .expireAfterWrite(10, TimeUnit.MINUTES) 
  6.  
  7. .removalListener(MY_LISTENER) 
  8.  
  9. .build( 
  10.  
  11. new CacheLoader<Key, Graph>() { 
  12.  
  13. @Override 
  14.  
  15. public Graph load(Key key) throws AnyException { 
  16.  
  17. return createExpensiveGraph(key); 
  18.  
  19.  
  20. }); 

适用性

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

一个 `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` :

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

查询 `LoadingCache` 的规范方法是使用 `get(K)` 方法。这将返回一个已经缓存的值,或者使用缓存的 `CacheLoader` 原子地将新值加载到缓存中。由于 `CacheLoader` 可能会抛出 `Exception`,因此 `LoadingCache.get(K)` 会抛出 `ExecutionException`。(如果缓存加载器抛出 unchecked 异常,则`get(K)` 会引发包装了 `UncheckedExecutionException` 的异常。)您还可以选择使用 `getUnchecked(K)` 将所有异常包装在 `UncheckedExecutionException` 中, 但是如果底层的 `CacheLoader` 通常会抛出受检查异常,这可能会导致令人惊讶的行为。

  1. LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() 
  2.  
  3. .expireAfterAccess(10, TimeUnit.MINUTES) 
  4.  
  5. .build( 
  6.  
  7. new CacheLoader<Key, Graph>() { 
  8.  
  9. public Graph load(Key key) { // no checked exception 
  10.  
  11. return createExpensiveGraph(key); 
  12.  
  13.  
  14. }); 
  15.  
  16. ... 
  17.  
  18. return graphs.getUnchecked(key); 

可以使用 `getAll(Iterable<? extends K>)` 方法执行批量查找。默认情况下,`getAll` 将为缓存中不存在的每个键单独发出 `CacheLoader.load` 调用。如果批量检索比许多单个查询更有效,则可以覆盖 `CacheLoader.loadAll` 来利用这一点。 `getAll(Iterable)` 的性能将相应提高。

请注意,您可以编写一个 `CacheLoader.loadAll` 实现,该实现加载未明确要求的键的值。例如,如果计算某个组中任何键的值给您该组中所有键的值,则 `loadAll` 可能会同时加载其余组。

使用 Callable

所有 Guava 缓存(无论是否加载)均支持方法 `get(K, Callable)`。此方法返回与缓存中的键关联的值,或从指定的 `Callable` 中计算出该值并将其添加到缓存中。在加载完成之前,不会修改与此缓存关联的可观察状态。此方法为常规的“如果已缓存,则返回;否则创建,缓存并返回”模式提供了简单的替代方法。

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

直接插入

可以直接使用 `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` 相同的限制外,请注意,权重是在条目创建时计算的,此后是静态的。

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

基于时间的驱逐

  • `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)` 指定某些将要在刷新时执行的明智行为,它允许您在计算新值时使用旧值。

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

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

 

责任编辑:张燕妮 来源: 今日头条
相关推荐

2014-09-26 14:30:41

2010-12-01 12:31:23

NetCat扫描端口

2009-07-21 14:16:18

Scalafor表达式

2013-04-11 10:51:27

2014-05-29 14:44:06

瑞士军刀综合征开发者

2013-06-08 10:36:47

Linux命令行

2017-05-03 14:45:45

MySQL数据恢复

2019-06-24 09:57:39

网络工具调试

2023-12-25 12:03:42

2019-06-27 17:00:09

nc命令 Linux

2011-10-18 14:11:17

Web开发

2022-02-15 10:15:13

Web网络程序员

2021-09-05 18:30:59

Alpine容器Busybox

2011-08-01 09:43:08

PhoneGap 1.PhoneGap

2023-04-27 07:06:09

Categraf夜莺

2015-09-28 09:46:31

ZooKeeper分布式系统瑞士军刀

2017-04-21 09:42:18

4G5G物联网

2021-12-28 09:55:40

UbuntuRescuezillaLinux

2020-11-07 16:30:27

Python开发程序员

2009-09-09 12:10:40

点赞
收藏

51CTO技术栈公众号