LocalCache
LocalCache(Guava Cache)是一个本地缓存,他不会进行文件存储或跨服务器共享,其主要用于提升单个服务节点的性能。
LocalCache 提供了两个核心接口 Cache 和 LoadingCache 来构建缓存。
手动管理缓存 Cache
Cache 接口是一种需要你手动管理缓存项。
当缓存的加载逻辑不固定,或者需要在特定时机才能确定缓存值时使用 Cache。
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
public class CacheExample {
public static void main(String[] args) throws Exception {
// 一个需要手动管理缓存项的缓存
Cache<String, String> cache = CacheBuilder.newBuilder()
// 设置缓存的最大容量
.maximumSize(100)
.build();
// 手动添加缓存数据、获取缓存k1
cache.put("k1", "v1");
String result1 = cache.get("k1", () -> "回调不会执行,因为缓存已经存在");
// 多次获不存在的缓存k2
String result2 = cache.get("k2", () -> {
String v = "v2";
System.out.println("数据不存在,从数据库加载..." + v);
return v;
});
String result3 = cache.get("k2", () -> "回调不会执行,因为缓存已经存在");
// 移除缓存k2、重新获取缓存k2
cache.invalidate("k2");
String result4 = cache.get("k2", () -> {
String v = "new-v2";
System.out.println("数据不存在,从数据库加载..." + v);
return v;
});
}
}自动加载缓存 LoadingCache
LoadingCache 接口扩展了自动加载缓存项的能力。
在构建 LoadingCache 时,需要提供一个缓存加载器 CacheLoader 。
当键值的计算和获取方式是固定的、可预期时使用 LoadingCache。
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
public class LoadingCacheExample {
public static void main(String[] args) throws Exception {
// 一个能自动加载缓存项的缓存,构建时提供一个缓存加载器
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(CacheLoader.from(k -> {
System.out.println("访问数据库获取值...");
return "value:" + k;
}));
// 第一次调用 get,缓存未命中,自动触发 CacheLoader.load()
String value1 = loadingCache.get("user-101");
System.out.println(value1);
// 第二次调用 get,缓存命中,直接返回结果
String value2 = loadingCache.get("user-101");
System.out.println(value2);
}
}缓存策略
LocalCache 可以组合多种策略来精细化地控制缓存的行为。
基于容量
当缓存容量达到上限时,采用LRU算法移除缓存项。
maximumSize:最大存储数量。maximumWeight:最大存储重量,需要提供一个称重器 weigher 用于计算缓存项的重量。
maximumSize 和 maximumWeight是互斥的策略无法同时使用。
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.Weigher;
import java.util.Random;
public class CacheRecycleCapacityExample {
public static void main(String[] args) throws Exception {
// 缓存项数量达到最大值时执行LRU淘汰缓存项
LoadingCache<Integer, String> cacheMaxNum = CacheBuilder.newBuilder()
.maximumSize(5)
.build(CacheLoader.from(k -> k + ""));
// cacheMaxNum 中缓存项的变化
for (int i = 1; i <= 10; i++) {
cacheMaxNum.get(i);
System.out.println(cacheMaxNum.asMap().values());
}
// 缓存项重量达到最大值时执行LRU淘汰缓存项(称重器:按照字符串长度计算重量)
LoadingCache<Integer, String> cacheMaxWeight = CacheBuilder.newBuilder()
.maximumWeight(10)
.weigher((Weigher<Integer, String>) (k, v) -> v.length())
.build(CacheLoader.from(k -> k + ""));
// cacheMaxWeight 中缓存项的变化
Random random = new Random();
for (int i = 1; i <= 10; i++) {
int ri = random.nextInt((int) Math.pow(10,i));
cacheMaxWeight.get(ri);
System.out.println(cacheMaxWeight.asMap().values());
}
}
}基于时间
expireAfterWrite:基于写入时间的过期回收缓存项,阻塞获取新缓存值。expireAfterAccess:基于访问时间的过期回收缓存项,阻塞获取新缓存值(读写重置时间)。refreshAfterWrite:基于写入时间的阻塞刷新,在新的缓存值返回前使用旧值。
| 组合使用 | 概括 | 缺点 |
|---|---|---|
rw + ea | 【首选】为热数据自动保鲜,为冷数据自动清理。 | 短暂返回旧数据 |
ew + ea | 保证数据强一致性,但牺牲了性能。 | 性能抖动、会阻塞、雪崩风险 |
rw + ew | 用于有绝对生命周期的场景,如会话和令牌。 | 雪崩风险、内存管理较弱 |
rw + ea 它能以最佳性能解决 90% 的缓存问题,只有需要强制安全或一致性时,才考虑后两种组合。
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
public class CacheRecycleTimeExample {
public static void main(String[] args) throws Exception {
// 1. 当容量超过上限,采用LRU回收缓存
// 2. 缓存项写入后3秒刷新
// 3. 缓存项读写后5秒过期
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(3, TimeUnit.SECONDS)
.expireAfterAccess(5, TimeUnit.SECONDS)
.build(CacheLoader.from(k -> {
System.out.println("获取缓存项");
return System.currentTimeMillis() + "";
}));
// 频繁访问,能确保数据较新
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
String value = cache.get("k");
System.out.println(value);
}
// 访问频率骤减,能确保数据有效
for (int i = 0; i < 10; i++) {
Thread.sleep(i * 1000);
String value = cache.get("k");
System.out.println(value);
}
}
}基于键值引用
weakKeys:使用弱引用包装键,当程序中不存在该键的强引用了,键值将在下一次GC时清理。weakValues:使用弱引用包装值,当程序中不存在该值的强引用了,键值将在下一次GC时清理。softValues:使用软引用包装值,当内存紧张时GC会回收软引用值。
缓存中存放的是包装实际键值的弱引用或软引用,当发现实际键值被GC清理后才会回收缓存。
非必要不要使用引用策略管理缓存
- 使用引用策略回收缓存,会大大增加缓存复杂度、难以预测、难以调试。
weakKeys与softValues是互斥的只能二选一使用。weakKeys对键判定依赖的是==而非.equals,这是该策略的一大坑。- 当键值为字面量或者常量时,引用策略会失效(常量池中不会被GC)。
- 当存在其他激进的策略,引用策略会失效(例如:expireAfterAccess(1, TimeUnit.MILLISECOND))
最后更新于