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 查看。