Guava Cache 是 Guava 中的一个内存缓存模块,用于将数据缓存到 JVM 内存中。它的架构设计灵感来源于 ConcurrentHashMap,因此也是一个线程安全的键值对缓存,还提供了缓存失效策略、缓存剔除策略、缓存动态加载、监控缓存加载、命中情况等额外功能。

使用示例

  1. public class GuavaCacheDemo {
  2. public static void main(String[] args) throws ExecutionException {
  3. LoadingCache<String, String> cache = CacheBuilder.newBuilder()
  4. // 设置并发级别为8,并发级别是指可以同时写缓存的线程数
  5. .concurrencyLevel(8)
  6. // 设置缓存容器的初始容量为10
  7. .initialCapacity(10)
  8. // 设置缓存最大容量为100,超过100之后就会按照LRU最近最少使用算法来移除缓存项
  9. .maximumSize(100)
  10. // 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
  11. .recordStats()
  12. // 设置写缓存后n秒钟过期
  13. .expireAfterWrite(60, TimeUnit.SECONDS)
  14. // 设置读写缓存后n秒钟过期,实际很少用到,一般只使用expireAfterWrite
  15. .expireAfterAccess(60, TimeUnit.SECONDS)
  16. // 设置写缓存后n秒钟刷新对应键值对
  17. .refreshAfterWrite(60, TimeUnit.SECONDS)
  18. // 设置缓存的移除通知
  19. .removalListener(notification -> {
  20. System.out.println("key is:" + notification.getKey() + ", value is:" + notification.getValue()
  21. + " 被移除! 原因:" + notification.getCause());
  22. })
  23. // build方法中可以指定CacheLoader,在缓存键不存在时通过CacheLoader的实现自动加载键对应的缓存值
  24. .build(new DemoCacheLoader());
  25. // 添加元素
  26. cache.put("a", "1");
  27. cache.put("b", "2");
  28. cache.put("c", "3");
  29. // 获取元素
  30. System.out.println(cache.get("a"));
  31. System.out.println(cache.get("d"));
  32. // 缓存失效
  33. cache.invalidate("c");
  34. System.out.println(cache.get("c"));
  35. // 刷新缓存
  36. cache.refresh("a");
  37. System.out.println(cache.get("a"));
  38. // 缓存命中状态查看
  39. System.out.println(cache.stats());
  40. }
  41. /**
  42. * 随机缓存加载
  43. */
  44. public static class DemoCacheLoader extends CacheLoader<String, String> {
  45. @Override
  46. public String load(String key) throws Exception {
  47. System.out.println(Thread.currentThread().getName() + " 加载" + key + "开始");
  48. TimeUnit.SECONDS.sleep(1);
  49. String value = String.valueOf(new Random().nextInt(100));
  50. System.out.println(Thread.currentThread().getName() + " 加载" + key + "结束");
  51. return value;
  52. }
  53. }
  54. }

CacheBuilder 采用了 builder 设计模式,它的每个方法都返回 CacheBuilder 本身,直到 build 方法被调用。该类中提供了很多的参数设置选项,可以设置 cache 的默认大小,并发数,存活时间,过期策略等等。

其中 LoadingCache 是 Cache 的子接口,相比较于 Cache,当从 LoadingCache 中读取一个指定 key 的记录时,如果该记录不存在可以自动执行加载数据到缓存的操作。当通过 CacheBuilder 构建 Guava Cache 时,如果返回的是 LoadingCache 类型,则在调用 CacheBuilder 的 build 方法时,必须传递一个 CacheLoader 的实现类,用来指定缓存加载的逻辑当然如果不想指定缓存重建策略,则可以使用无参的 build() 方法,它将返回 Cache 类型的构建对象。

配置解析

1. initialCapacity

用来设置内部哈希表的最小总大小。例如,如果初始容量为 60,并发级别为 8,则会创建八个分段,每个分段有一个大小为 8 的哈希表。如果在构建时提供足够大的话可以避免后续进行昂贵的调整大小操作的需要,但将此值设置为不必要的高会浪费内存。

  1. CacheBuilder.newBuilder()
  2. .initialCapacity(60)
  3. .build();

2. concurrencyLevel

�并发级别,允许指定数量的并发更新而不会产生锁争用。如果并发级别为 8,内部会创建 8 个哈希表分段,每个分段由自己的写锁进行控制。这种设计思想类似 JDK 8 之前的 ConcurrentHashMap 的分段锁实现。即使将并发级别设置为 1,一次仅允许一个线程修改缓存,但由于读取操作和缓存加载计算可以并发进行,因此与完全同步相比,仍会产生更高的并发性。

  1. CacheBuilder.newBuilder()
  2. // 设置并发级别为cpu核心数
  3. .concurrencyLevel(Runtime.getRuntime().availableProcessors())
  4. .build();

3. 缓存容量

Guava Cache 可以在构建缓存对象时指定缓存所能够存储的最大记录数量。当 Cache 中的记录数量达到最大值后如果再调用 put 方法向其中添加对象,Guava 会先从当前缓存的对象记录中选择一条删除掉,腾出空间后再将新的对象存储到 Cache 中。缓存删除策略有如下两种:

  • 基于容量的清除:当通过 CacheBuilder.maximumSize(long) 方法设置 Cache 的最大容量数时,当缓存数量达到或接近最大值时,Cache 将清除掉那些最近最少(FIFO)使用的缓存


  1. CacheBuilder.newBuilder()
  2. // 设置缓存最大容量
  3. .maximumSize(1000)
  4. .build();
  • 基于权重的清除:当使用 CacheBuilder.weigher(Weigher) 指定一个权重函数,并且用CacheBuilder.maximumWeight(long) 指定最大总重。比如每一项缓存所占据的内存空间大小都不一样,可以看作它们有不同的权重,当超过最大总重后触发基于权重的清除策略。


  1. CacheBuilder.newBuilder()
  2. .weigher(new Weigher<String, String>() {
  3. @Override
  4. public int weigh(String key, String value) {
  5. return key.length();
  6. }
  7. })
  8. .maximumWeight(10)
  9. .build();

4. 缓存定期清除策略

  • expireAfterWrite:当缓存项被创建或更新其值后经过固定持续时间,自动从缓存中删除该项。但在实现上不是实时的。


  • expireAfterAccess:缓存项被创建、更新其值或者被访问后经过固定持续时间,自动从缓存中删除该项。读写操作包括 get 和 put 方法,但不包括 containsKey,也不是包括对集合视图的操作。因此,像遍历这种操作不会重置访问时间。
  1. CacheBuilder.newBuilder()
  2. // 设置写缓存后60秒钟过期
  3. .expireAfterWrite(60, TimeUnit.SECONDS)
  4. // 设置读写缓存后60秒钟过期,实际很少用到,一般只使用expireAfterWrite
  5. .expireAfterAccess(60, TimeUnit.SECONDS)
  6. .build();

5. refreshAfterWrite

当缓存项被创建或更新其值后经过固定持续时间,进行自动刷新。注意:刷新动作默认实现是同步的。

  1. CacheBuilder.newBuilder()
  2. // 设置写缓存后60秒钟刷新对应键值对
  3. .refreshAfterWrite(60, TimeUnit.SECONDS)
  4. .build();

refreshAfterWrite 通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新。因此,如果同时声明了expireAfterWrite 和 refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。

测试代码示例:

  1. public static void main(String[] args) {
  2. LoadingCache<String, String> cache = CacheBuilder.newBuilder()
  3. // 设置1s刷新
  4. .refreshAfterWrite(1, TimeUnit.SECONDS)
  5. .removalListener(notification -> {
  6. System.out.println("key is:" + notification.getKey() + ", value is:" + notification.getValue()
  7. + " 被移除! 原因:" + notification.getCause());
  8. })
  9. .build(new CacheLoader<String, String>() {
  10. @Override
  11. public String load(String key) throws Exception {
  12. System.out.println(Thread.currentThread().getName() + " 加载" + key + "开始");
  13. String value = String.valueOf(new Random().nextInt(100));
  14. System.out.println(Thread.currentThread().getName() + " 加载" + key + "结束");
  15. return value;
  16. }
  17. });
  18. // 添加元素
  19. cache.put("a", "1");
  20. // 等待5s后获取元素
  21. new Thread(() -> {
  22. try {
  23. Thread.sleep(1000 * 5);
  24. System.out.println(cache.get("a"));
  25. } catch (Exception e) {
  26. //
  27. }
  28. }).start();
  29. }

运行代码可以发现,在 1 秒后 cache 并没有刷新,而是等到获取元素时才开始刷新。

刷新操作默认是同步的,其内部调用的是 CacheLoader 的 reload方法。如果不想在刷新时影响索引操作,可以在 CacheLoader 的 reload 方法中将加载逻辑实现为异步,这样检索不会被刷新操作所拖慢了。

  1. @Override
  2. public ListenableFuture<Object> reload(String key, Object oldValue) throws Exception {
  3. ListenableFutureTask<Object> task = ListenableFutureTask.create(() -> load(key));
  4. Executors.newCachedThreadPool().execute(task);
  5. // 外部会通过 task.isDone() 方法来判断任务是否完成
  6. return task;
  7. }

6. removalListener

当缓存项被删除或被更新时触发的监听器。默认情况下,监听器方法是在移除缓存时同步调用的,因为缓存的维护和请求响应通常是同时进行的。如果想要异步调用监听器方法,可以用 RemovalListeners 提供的 asynchronous(RemovalListener, Executor) 方法把监听器装饰为异步操作。

  1. CacheBuilder.newBuilder()
  2. .removalListener(RemovalListeners.asynchronous(notification -> {
  3. System.out.println("key is:" + notification.getKey() + ", value is:" + notification.getValue()
  4. + " 被移除! 原因:" + notification.getCause());
  5. }, Executors.newSingleThreadExecutor()))
  6. .build();

这里提一下 Guava Cache 的自动回收,实际上并不是缓存项过期了以后就马上清理掉,而是在读或写的时候做少量的维护工作,这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程定时检测,但这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样 CacheBuilder 就不可用了。

相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用 Cache.cleanUp()。ScheduledExecutorService 可以帮助你很好地实现这样的定时调度。

7. recordStats

可以对 Cache 的命中率、加载数据时间等信息进行统计。在构建 Cache 对象时,通过 recordStats 方法可以开启统计信息的开关。开关开启后 Cache 会自动对缓存的各种操作进行统计,调用 Cache 的 stats 方法就可以查看统计后的信息。

  1. CacheBuilder.newBuilder()
  2. .recordStats()
  3. .build();

Cache 的 stats 方法返回一个 CacheStats 对象,提供了如下方法:

  1. // 缓存查找总次数,其值为hitCount + missCount
  2. public long requestCount()
  3. // 缓存命中次数
  4. public long hitCount()
  5. // 缓存命中率
  6. public double hitRate()
  7. // 缓存未命中次数
  8. public long missCount()
  9. // 缓存未命中率
  10. public double missRate()
  11. // 缓存加载次数、成功次数、失败次数、失败率、总耗时、平均时间
  12. public long loadCount()
  13. public long loadSuccessCount()
  14. public long loadExceptionCount()
  15. public double loadExceptionRate()
  16. public long totalLoadTime()
  17. public double averageLoadPenalty()
  18. // 缓存被回收的次数,不包括显式清除
  19. public long evictionCount()

常用方法

  1. /**
  2. * 返回与此缓存中的key关联的值,如有必要会先加载该值。
  3. * 如果其他线程同时调用get或getUnchecked方法,只会有一个线程加载值
  4. */
  5. V get(K key) throws ExecutionException;
  6. V getUnchecked(K key);
  7. /**
  8. * 显式清除缓存项。包括:单个清除、批量清除、全部清除
  9. */
  10. void invalidate(@CompatibleWith("K") Object key);
  11. void invalidateAll(Iterable<? extends Object> keys);
  12. void invalidateAll();
  13. /**
  14. * 显式执行缓存所需的任何挂起的维护操作,比如清除过期缓存
  15. */
  16. void cleanUp();
  17. /**
  18. * 显式刷新缓存键,内部调用CacheLoader的reload方法
  19. */
  20. void refresh(K key);

参考链接:https://github.com/google/guava/wiki/CachesExplained#refresh