Guava 缓存简介
1. 概述
在本教程中,我们将了解Guava Cache的实现——基本用法、删除策略、刷新缓存和一些有趣的批量操作。
最后,我们将看看使用缓存能够发送的删除通知。
2. 如何使用 Guava 缓存
让我们从一个简单的例子开始——让我们缓存String实例的大写形式。
首先,我们将创建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”键在缓存中没有值——因此该值被计算和缓存。
另请注意,我们正在使用*getUnchecked()*操作 - 如果该值不存在,它会计算并将值加载到缓存中。
3. 删除政策
每个缓存都需要在某个时候删除值。让我们讨论一下从缓存中逐出值的机制——使用不同的标准。
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"));
}
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. 按时间删除
除了使用大小来删除旧记录,我们还可以使用时间。在以下示例中,我们自定义缓存以删除已空闲 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"));
}
我们还可以根据它们的总生存时间删除记录。在以下示例中,缓存将在存储 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. 弱键
接下来,让我们看看如何使我们的缓存键具有弱引用——允许垃圾收集器收集其他地方没有引用的缓存键。
默认情况下,缓存键和值都具有强引用,但我们可以使用*weakKeys()*使缓存使用弱引用存储键,如下例所示:
@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);
}
5. 软价值
我们可以让垃圾收集器通过使用*softValues()*来收集我们的缓存值,如下例所示:
@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值
现在,让我们看看如何处理缓存null值。默认情况下,如果您尝试加载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);
}
7. 刷新缓存
接下来,让我们看看如何刷新我们的缓存值。
7.1. 手动刷新
我们可以在*LoadingCache.refresh(key)*的帮助下手动刷新单个键。
String value = loadingCache.get("key");
loadingCache.refresh("key");
这将强制CacheLoader加载键的新值。
在成功加载新值*之前,该key的先前值将由*get(key)返回。
7.2. 自动刷新
我们可以使用*CacheBuilder.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);
}
重要的是要了解*refreshAfterWrite(duration)仅使密钥在指定的 duration 之后符合刷新条件。只有当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. 移除通知
有时,当一条记录从缓存中删除时,您需要采取一些措施;所以,让我们讨论一下 RemovalNotification。
我们可以注册一个RemovalListener来获取记录被删除的通知。我们还可以通过*getCause()*方法访问删除的原因。
在以下示例中,当缓存中的第四个元素因其大小而被接收时,会收到RemovalNotification :
@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());
}
10. 笔记
最后,这里有一些关于 Guava 缓存实现的额外快速说明:
- 它是线程安全的
- 您可以使用*put(key,value)*手动将值插入缓存
- 您可以使用*CacheStats(hitRate(), missRate(), ..)*来衡量您的缓存性能