1. 概述

本文将深入探讨 Guava Cache 的使用方式,包括基础用法、缓存淘汰策略、缓存刷新机制以及一些实用的批量操作。

最后我们还会介绍如何监听缓存移除事件,获取通知信息。

2. 如何使用 Guava Cache

我们先从一个简单的例子入手:缓存字符串的大写形式。

首先创建一个 CacheLoader,用于定义当缓存中没有值时如何计算并加载数据。然后借助 CacheBuilder 构建缓存实例:

@Test
public void whenCacheMiss_thenValueIsComputed() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().build(loader);

    assertEquals(0, cache.size());
    assertEquals("HELLO", cache.getUnchecked("hello"));
    assertEquals(1, cache.size());
}

可以看到,当我们第一次访问 "hello" 这个 key 时,缓存中并没有值,于是通过 load() 方法计算并缓存了结果。

注意我们使用的是 getUnchecked() 方法,它会在缓存未命中时自动调用 load() 加载值。

3. 缓存淘汰策略

每个缓存都需要在某个时刻淘汰旧数据。Guava 提供了几种常见的淘汰策略。

3.1. 按大小淘汰

可以使用 maximumSize() 限制缓存最大条目数。一旦超过限制,最老的数据会被移除。

以下示例将缓存大小限制为 3 条记录:

@Test
public void whenCacheReachMaxSize_thenEviction() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().maximumSize(3).build(loader);

    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("forth");
    assertEquals(3, cache.size());
    assertNull(cache.getIfPresent("first"));
    assertEquals("FORTH", cache.getIfPresent("forth"));
}

✅ 当缓存满了后,最早插入的 "first" 被淘汰。

3.2. 按权重淘汰

还可以使用自定义权重函数来控制缓存容量。下面的例子中,我们将字符串长度作为权重:

@Test
public void whenCacheReachMaxWeight_thenEviction() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    Weigher<String, String> weighByLength;
    weighByLength = new Weigher<String, String>() {
        @Override
        public int weigh(String key, String value) {
            return value.length();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .maximumWeight(16)
      .weigher(weighByLength)
      .build(loader);

    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("last");
    assertEquals(3, cache.size());
    assertNull(cache.getIfPresent("first"));
    assertEquals("LAST", cache.getIfPresent("last"));
}

⚠️ 注意:为了给新值腾空间,缓存可能会一次性移除多个条目。

3.3. 按时间淘汰

除了按大小淘汰,也可以按时间进行淘汰。

✅ 空闲时间淘汰(idle time)

下面这个例子设置缓存项在空闲 2ms 后被移除:

@Test
public void whenEntryIdle_thenEviction()
  throws InterruptedException {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .expireAfterAccess(2,TimeUnit.MILLISECONDS)
      .build(loader);

    cache.getUnchecked("hello");
    assertEquals(1, cache.size());

    cache.getUnchecked("hello");
    Thread.sleep(300);

    cache.getUnchecked("test");
    assertEquals(1, cache.size());
    assertNull(cache.getIfPresent("hello"));
}

✅ 存活时间淘汰(live time)

或者设置缓存在写入 2ms 后过期:

@Test
public void whenEntryLiveTimeExpire_thenEviction()
  throws InterruptedException {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .expireAfterWrite(2,TimeUnit.MILLISECONDS)
      .build(loader);

    cache.getUnchecked("hello");
    assertEquals(1, cache.size());
    Thread.sleep(300);
    cache.getUnchecked("test");
    assertEquals(1, cache.size());
    assertNull(cache.getIfPresent("hello"));
}

4. 弱引用 Key

默认情况下,缓存中的 key 和 value 都是强引用。我们可以使用 weakKeys() 将 key 设置为弱引用,这样 JVM 在 GC 时就能回收这些 key:

@Test
public void whenWeakKeyHasNoRef_thenRemoveFromCache() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().weakKeys().build(loader);
}

⚠️ 弱引用 key 可能会导致频繁 GC,适用于特定场景。

5. 软引用 Value

同样地,可以用 softValues() 让 value 使用软引用,允许 JVM 在内存紧张时回收它们:

@Test
public void whenSoftValue_thenRemoveFromCache() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().softValues().build(loader);
}

⚠️ 软引用过多可能影响性能,推荐优先使用 maximumSize() 控制缓存大小。

6. 处理 null 值

Guava Cache 默认不允许缓存 null 值,否则会抛出异常。但如果业务上 null 有意义,可以用 Optional 包装:

@Test
public void whenNullValue_thenOptional() {
    CacheLoader<String, Optional<String>> loader;
    loader = new CacheLoader<String, Optional<String>>() {
        @Override
        public Optional<String> load(String key) {
            return Optional.fromNullable(getSuffix(key));
        }
    };

    LoadingCache<String, Optional<String>> cache;
    cache = CacheBuilder.newBuilder().build(loader);

    assertEquals("txt", cache.getUnchecked("text.txt").get());
    assertFalse(cache.getUnchecked("hello").isPresent());
}
private String getSuffix(final String str) {
    int lastIndex = str.lastIndexOf('.');
    if (lastIndex == -1) {
        return null;
    }
    return str.substring(lastIndex + 1);
}

✅ 使用 Optional.absent() 替代 null,避免异常。

7. 刷新缓存

7.1. 手动刷新

可以通过 refresh(key) 强制刷新指定 key 的值:

String value = loadingCache.get("key");
loadingCache.refresh("key");

⚠️ 刷新期间,旧值仍可被访问,直到新值加载完成。

7.2. 自动刷新

使用 refreshAfterWrite(duration) 可以设定缓存在写入一段时间后自动刷新:

@Test
public void whenLiveTimeEnd_thenRefresh() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .refreshAfterWrite(1,TimeUnit.MINUTES)
      .build(loader);
}

⚠️ 自动刷新只有在调用 get(key) 时才会真正触发。

8. 预加载缓存

可以使用 putAll(Map) 一次性插入多个键值对:

@Test
public void whenPreloadCache_thenUsePutAll() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().build(loader);

    Map<String, String> map = new HashMap<String, String>();
    map.put("first", "FIRST");
    map.put("second", "SECOND");
    cache.putAll(map);

    assertEquals(2, cache.size());
}

✅ 适用于启动时预热缓存的场景。

9. 移除监听器 RemovalListener

有时我们需要在缓存项被移除时做一些清理工作。可以通过注册 RemovalListener 来监听:

@Test
public void whenEntryRemovedFromCache_thenNotify() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(final String key) {
            return key.toUpperCase();
        }
    };

    RemovalListener<String, String> listener;
    listener = new RemovalListener<String, String>() {
        @Override
        public void onRemoval(RemovalNotification<String, String> n){
            if (n.wasEvicted()) {
                String cause = n.getCause().name();
                assertEquals(RemovalCause.SIZE.toString(),cause);
            }
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .maximumSize(3)
      .removalListener(listener)
      .build(loader);

    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("last");
    assertEquals(3, cache.size());
}

✅ 通过 getCause() 可以知道是哪种原因导致的移除(如 SIZE、EXPIRED 等)。

10. 注意事项

以下是一些使用 Guava Cache 的常见要点:

  • ✅ 线程安全:所有操作都是线程安全的。
  • ✅ 手动插入:支持 put(key, value) 手动插入数据。
  • ✅ 性能监控:可通过 CacheStats 获取命中率、未命中率等指标。

11. 小结

本文全面介绍了 Guava Cache 的核心功能,包括:

  • ✅ 基础使用与加载机制
  • ✅ 多种淘汰策略(size、weight、time)
  • ✅ 缓存刷新与预加载
  • ✅ null 值处理与监听移除事件

完整代码示例可在 GitHub 查看。


原始标题:Guava Cache

« 上一篇: Baeldung Weekly 40
» 下一篇: Baeldung周报41