Caffeine是一个高性能的本地缓存库,提供了四种类型的Cache,对应着四种加载策略:手动加载(Cache)、自动加载(LoadingCache)、手动异步加载(AsyncCache)和自动异步加载(AsyncLoadingCache);
相较于Redis,Caffeine可以从策略配置上来避免缓存击穿(key过期后大流量并发访问场景)。
1. 策略配置效果
策略配置效果:【已验证】
2. 测试Demo
注意load实现中getByActivityId0()方法,num值自增!
@Slf4j
@RestController
@RequestMapping("/caffeine")
public class CaffeineController {
@Autowired
private ActivityClientRepositoryImpl activityClientRepository;
@PostMapping("/get")
public Object query(String id) {
LoadingCache<String, ActivityModel> activityCache = activityClientRepository.getActivityCache();
System.out.println("before cache=" + activityCache.asMap());
ActivityModel model = activityClientRepository.getActivityFromCache(id);
System.out.println("after cache=" + activityCache.asMap());
return model;
}
}
@Data
@Repository
public class ActivityClientRepositoryImpl {
private long activityCacheMaximumSize;
private int activityCacheExpireAfterAccess;
private int activityCacheRefreshAfterWrite;
public LoadingCache<String, ActivityModel> activityCache;
private AtomicInteger count = new AtomicInteger(0);
@PostConstruct
private void init() {
activityCache = Caffeine.newBuilder()
.maximumSize(activityCacheMaximumSize)
.expireAfterAccess(20, TimeUnit.SECONDS)
.refreshAfterWrite(5, TimeUnit.SECONDS)
.build(this::getByActivityId0); // 自动加载策略
}
private ActivityModel getByActivityId0(String activityId) throws InterruptedException {
//TimeUnit.SECONDS.sleep(1);
System.out.println("load key=" + activityId);
ActivityModel activity = new ActivityModel();
activity.setNum(count.incrementAndGet());
return activity;
}
public ActivityModel getActivityFromCache(String activityId) {
if (activityId == null) {
return null;
}
return activityCache.get(activityId);
}
@Value("${scd.activityCache.maximumSize:10000}")
public void setActivityCacheMaximumSize(long maximumSize) {
if (activityCache != null) { // Caffeine支持在程序运行过程中策略的配置!
activityCache.policy().eviction().ifPresent(eviction -> {
eviction.setMaximum(maximumSize);
});
}
this.activityCacheMaximumSize = maximumSize;
}
@Value("${scd.activityCache.expireAfterAccess:3000}")
public void setActivityCacheExpireAfterAccess(int expireAfterAccess) {
if (activityCache != null) {
activityCache.policy().expireAfterAccess().ifPresent(expiration -> {
expiration.setExpiresAfter(expireAfterAccess, TimeUnit.SECONDS);
});
}
this.activityCacheExpireAfterAccess = expireAfterAccess;
}
@Value("${scd.activityCache.refreshAfterWrite:10}")
public void setActivityCacheRefreshAfterWrite(int refreshAfterWrite) {
if (activityCache != null) {
activityCache.policy().refreshAfterWrite().ifPresent(refresh -> {
refresh.setExpiresAfter(refreshAfterWrite, TimeUnit.SECONDS);
});
}
this.activityCacheRefreshAfterWrite = refreshAfterWrite;
}
}
3. 测试效果
3.1 LoadingCache自身特性
LoadingCache特性
1. key不存在时,调用get(key)方法会执行load方法进行加载;
before cache={}
load key=10
after cache={10=ActivityModel(num=1)}
2. key不存在时,两个线程同时执行get(key)方法,后一线程被阻塞,直至前一线程更新缓存完成!
3.2 expireAfterAccess驱逐策略
expireAfterAccess驱逐策略
策略配置:.expireAfterAccess(10, TimeUnit.SECONDS)
1. key过期后,调用get(key)方法也会执行load方法进行加载;
// 0s: get(10)
before cache={}
load key=10
after cache={10=ActivityModel(num=1)}
// 10s后:get(10)
before cache={}
load key=10
after cache={10=ActivityModel(num=2)}
3.3 refreshAfterWrite刷新策略
refreshAfterWrite刷新策略:
策略配置:.refreshAfterWrite(10, TimeUnit.SECONDS)
// 为了效果明显,load实现中加了TimeUnit.SECONDS.sleep(5);
1. key过期后,第一次get(key)拿到的是旧值,同时异步调用load方法进行刷新
// 0s: get(11)
before cache={}
load key=11
after cache={11=ActivityModel(num=1)}
// 10s后:第一次get(11):
before cache={11=ActivityModel(num=1)}
after cache={11=ActivityModel(num=1)} // 这里:先返回,后异步load
load key=11
2. key过期后,第一次get(key)拿到的是旧值,紧接着get(key)拿到的也是旧值,直到上一次的load刷新成功
// 0s: get(11)
before cache={}
load key=11
after cache={11=ActivityModel(num=1)}
// 10s后:第一次get(11):
before cache={11=ActivityModel(num=1)}
after cache={11=ActivityModel(num=1)} // 这里:先返回,后异步load
// 紧接着的一次get(11):
before cache={11=ActivityModel(num=1)}
after cache={11=ActivityModel(num=1)}
load key=11 // 过了一会,第一次get时完成了异步load
3.4 混合双打
策略配置:
.expireAfterAccess(20, TimeUnit.SECONDS)
.refreshAfterWrite(5, TimeUnit.SECONDS)
// 为了效果明显,load实现中加了TimeUnit.SECONDS.sleep(1);
1. 直接模拟走一遍
// 0s: get(21)
before cache={}
load key=21 // key不存在,同步load
after cache={21=ActivityModel(num=1)}
// 1s: get(21)
before cache={21=ActivityModel(num=1)}
after cache={21=ActivityModel(num=1)}
// 6s: get(21)
before cache={21=ActivityModel(num=1)}
after cache={21=ActivityModel(num=1)} // 返回旧值,异步load
load key=21
// 8s: get(21)
before cache={21=ActivityModel(num=2)}
after cache={21=ActivityModel(num=2)}
// 28s: get(21)
before cache={}
load key=21 // key过期了,同步load
after cache={21=ActivityModel(num=3)}
4. 总结
Refer:Guava Cache特性:refreshAfterWrite与expireAfterWrite
5. cache.getAll(keys) 测试
@PostMapping("/getAll")
public Object queryAll(String ids) {
System.out.println("before cache=" + activityCache.asMap());
String[] split = ids.split(",");
List<String> collect = Arrays.stream(split).collect(Collectors.toList());
Map<String, ActivityModel> all = activityCache.getAll(collect);
System.out.println("after cache=" + activityCache.asMap());
return all;
}
@PostConstruct
private void init() {
activityCache = Caffeine.newBuilder()
.maximumSize(1024)
.expireAfterAccess(200, TimeUnit.SECONDS)
.refreshAfterWrite(50, TimeUnit.SECONDS)
.build(new CacheLoader<String, ActivityModel>() {
@Nullable
@Override
public ActivityModel load(@NotNull String key) throws Exception {
return getByActivityId0(key);
}
@NotNull
@Override
public Map<String, ActivityModel> loadAll(@NotNull Iterable<? extends String> keys) throws Exception {
System.out.println("loadAll keys=" + keys);
Map<String, ActivityModel> map = new HashMap<>();
for (String key : keys) {
ActivityModel activityModel = new ActivityModel();
activityModel.setNum(Integer.valueOf(key));
map.put(key, activityModel);
}
return map;
}
});
}
结论:
1. 未覆盖loadAll方法时,调用getAll(keys)方法时会对不存在的key值,依次调用load方法,同时返回值放入cache
// get(1)
before cache={}
load key=1
after cache={1=ActivityModel(num=1)}
// getAll(1,2,3)
before cache={1=ActivityModel(num=1)}
load key=2
load key=3
after cache={1=ActivityModel(num=1), 2=ActivityModel(num=2), 3=ActivityModel(num=3)}
2. 覆盖loadAll方法时,调用getAll(keys)时,会对不存在的keys,调用loadAll方法,同时返回值放入cache
// get(1)
before cache={}
load key=1
after cache={1=ActivityModel(num=1)}
// getAll(1,2,3)
before cache={1=ActivityModel(num=1)}
loadAll keys=[2, 3]
after cache={1=ActivityModel(num=1), 2=ActivityModel(num=2), 3=ActivityModel(num=3)}
Refer:Caffeine批量加载浅析 (实际应用场景)