1.简介
在提高并发、降低接口耗时和保护MySql数据库等场景中,缓存是一个常用手段,本地缓存则是缓存中一个不可忽视的技术方案。本地缓存常用的有Guava,而Caffeine则是它的升级版,提供更丰富的特性以及更优的淘汰算法。
2.常用淘汰算法
2.1 LRU(Least Recently Used)
LRU算法会记录每个数据的访问时间戳,当达到缓存使用上限时,会优先淘汰最老的数据。该算法适用于局部流量高峰的场景。如果类似全量访问的场景,则会导致缓存污染.同时,无法应对周期性访问的请求。
2.2 LFU(Least Frequently Used)
LFU算法会记录每个数据的访问次数,需要淘汰数据时优先淘汰访问次数最少的数据。该算法适合于存在高频访问的数据的场景,能够有效应对全量访问带来的缓存污染,但是没法应对局部流量高峰。同时访问次数的维护也是一大消耗。
2.3 TinyLFU
为了降低LFU算法中访问次数统计信息的空间占用,TinyLFU使用CountMinSketch算法近似实现了访问次数的统计。算法类似布隆过滤器,只是将布隆过滤器中的bit换成4bit,一个long存了16个4bit的频率统计值. 部分源码:
com.github.benmanes.caffeine.cache.FrequencySketch
// 频率存储,类似布隆过滤器中的bitmap
long[] table;
// 获取某个元素的频率
// 使用hash值算了四个位置,返回四个位置里最小的频率
public int frequency(@Nonnull E e) {
if (isNotInitialized()) {
return 0;
}
int hash = spread(e.hashCode());
int start = (hash & 3) << 2;
int frequency = Integer.MAX_VALUE;
for (int i = 0; i < 4; i++) {
int index = indexOf(hash, i);
int count = (int) ((table[index] >>> ((start + i) << 2)) & 0xfL);
frequency = Math.min(frequency, count);
}
return frequency;
}
// 增加统计频率
// 根据hash酸的四个位置的频率值都+1,如果已经达到最大频率值15,则不加
// 如果四个位置任一一个没达到最大值且数据已满,则所有频率值衰减为原来的一半
public void increment(@Nonnull E e) {
if (isNotInitialized()) {
return;
}
int hash = spread(e.hashCode());
int start = (hash & 3) << 2;
// Loop unrolling improves throughput by 5m ops/s
int index0 = indexOf(hash, 0);
int index1 = indexOf(hash, 1);
int index2 = indexOf(hash, 2);
int index3 = indexOf(hash, 3);
boolean added = incrementAt(index0, start);
added |= incrementAt(index1, start + 1);
added |= incrementAt(index2, start + 2);
added |= incrementAt(index3, start + 3);
if (added && (++size == sampleSize)) {
reset();
}
}
2.4 W-TinyLFU
W-TinyLFU在TinyLFU的基础上引入了LRU的特性,从而能够应对突发性流量。使用LRU策略的区域占总缓存空间的1%,剩余的99%则是使用LFU策略。Main Cache进一步分为Protected Cache(保护区)和Probation Cache(考察区),保护区占了Main Cache的80%。新增的数据,先放入LRU区域,如果Window Cache满了且考察区也满了,LRU淘汰的数据则和TinyLFU中的频率最低的PK,频率更低的被淘汰,高的进入考察区。考察区的数据访问频率超过阈值,则会进入保护区,如果保护区也满了,则选出最小频率的数据放入考察区,如果考察区也满了就要pk.有点像JVM堆内存分代管理的感觉。
3 实践
本地缓存caffeine+redis分布式缓存实现的多级缓存
// 初始化缓存
private final LoadingCache<Long, Optional<ProductData>> localProductDataCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build(new CacheLoader<Long, Optional<ProductData>>() {
@Override
public Optional<ProductData> load(final Long bgId) {
final ProductData productData = getProductData(brandGood);
if (productData == null) {
return Optional.absent();
}
return Optional.of(productData);
}
@Override
public Map<Long, Optional<ProductData>> loadAll(
final Iterable<? extends Long> bgIds) throws Exception {
// 批量接口实现......
}
});
private Optional<ProductData> getProductData(final long bgId) {
Optional<ProductData> dataOpt = redisCache.get(bgId, ProductData.class);
if (dataOpt.isPresent()) {
return dataOpt;
} else {
dataOpt = getProductDataFromRPC(bgId);
}
if (dataOpt.isPresent()) {
redisCache.put(bgId, productData, EXPIRE_TIME);
}
return dataOpt;
}