Guava Cache
这个是一开始大家用的本地缓存框架,他极大的简化了我们操作缓存时需要考虑的丢弃策略、刷新策略等等....
并且当时这个框架的各方面性能也是要高于Ehcache2框架的
但是时代再次发生了变化 ----------------------------------------华丽的分割线
Caffeine 框架横空出世
显来几个性能测试报告来做开胃菜
GuavaCache 和 Caffeine两个框架最根本的区别就在于淘汰策略的算法上:
- LRU 算法(GuavaCache使用的算法)
在LRUHashMap提到过,LRU(最近最少被使用),使用队列存储数据项,每次会把被访问的数据移到队头,淘汰时直接淘汰队尾的数据。但偶发性的、周期性的批量操作会导致 LRU 命中率急剧下降。
LRU的优点和局限性 :LRU可以很好的应对突发流量的情况,因为他不需要累计数据频率。但LRU通过历史数据来预测未来是局限的,它会认为最后到来的数据是最可能被再次访问的,从而给与它最高的优先级。
- LFU 算法
LFU(最不经常使用)。他的核心思想是“如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小” ,会记录数据访问的次数,当需要进行淘汰操作时,淘汰掉访问次数最少的数据。
LFU和LRU算法的不同之处,LRU的淘汰规则是基于访问时间,而LFU是基于访问次数的。
缺点:LFU算法根据次数进行缓存淘汰,还是以热点数据为例,某天有明星XXX出轨,XXX这个词被搜索了十万次,过了一个月后,热度过去了,大家搜索量少了,但XXX明星出轨的相关数据依然在缓存中,这份数据可能需要很久才能被淘汰掉。另外,LFU 算法由于需要额外的存储空间记录访问次数,数据量非常大的情况下对于存储的消耗也是很大的。
W-TinyLFU 算法(Caffeine使用的算法):
在W-TinyLFU中,数据首先会进入到 Window LRU, 从 Window LRU 中淘汰后,会进入到过滤器中过滤,当新来的数据比要驱逐的数据高频时,这个数据才会被缓存接纳,这么做的目的主要是为了使新数据积累一定的访问频率,以便于通过过滤器,进入到后面的缓存段中。
W-TinyLFU 使用Count-Min Sketch算法作为过滤器,该算法 是布隆过滤器的一种变种。
在Caffeine的实现中,会先创建一个Long类型的数组,数组的大小为 2,数组的大小为数据的个数,如果你的缓存大小是100,他会生成一个long数组大小是和100最接近的2的幂的数,也就是128。另外,Caffeine将64位的Long类型划分为4段,每段16位,用于存储4种hash算法对应的数据访问频率计数。
- 分段LRU(SLRU)
对于长期保留的数据,W-TinyLFU 使用了分段 LRU 策略。起初,一个数据项存储被存储在试用段(ProbationDeque)中,在后续被访问到时,它会被提升到保护段(ProtectedDeque)中(保护段占总容量的 80%)。保护段满后,有的数据会被淘汰回试用段,这也可能级联的触发试用段的淘汰。这套机制确保了访问间隔小的热数据被保存下来,而被重复访问少的冷数据则被回收。
- 读写优化
Guava Cache读写时会夹杂着缓存淘汰的操作,所以在读写操作时会浪费一部分性能。在Caffine中,这些事件操作都是异步的,他将这些事件提交到队列中。然后会通过默认的ForkJoinPool.commonPool(),或者自己配置线程池,进行取队列操作,然后在进行后续的淘汰,过期操作。读和写操作分别有自己的队列。
- readBuffer
读队列采用 RingBuffer,为了进一步减少读并发,采用多个 RingBuffer(striped ring buffer 条带环形缓冲),通过线程 id 哈希到对应的RingBuffer。环形缓存的一个显著特点是不需要进行 GC,直接通过覆盖过期数据。当一个 RingBuffer 容量满载后,会触发异步的执行操作,而后续的对该 ring buffer 的写入会被丢弃,直到这个 ring buffer 可被使用,因此 readBuffer 记录读缓存事务是有损的。因为读记录是为了优化驱策策略,允许他有损。
- writeBuffer
写队列采用传统的有界队列 ArrayQueue。
使用
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>
缓存填充策略
Caffeine Cache提供了三种缓存填充策略:手动、同步加载和异步加载。
- 手动加载
在每次get key的时候指定一个同步的函数,如果key不存在就调用这个函数生成一个值。
/**
* 手动加载
* @param key
* @return
*/
public Object manulOperator(String key) {
Cache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.expireAfterAccess(1, TimeUnit.SECONDS)
.maximumSize(10)
.build();
//如果一个key不存在,那么会进入指定的函数生成value
Object value = cache.get(key, t -> setValue(key).apply(key));
cache.put("hello",value);
//判断是否存在如果不存返回null
Object ifPresent = cache.getIfPresent(key);
//移除一个key
cache.invalidate(key);
return value;
}
public Function<String, Object> setValue(String key){
return t -> key + "value";
}
- 同步加载
构造Cache时候,build方法传入一个CacheLoader实现类。实现load方法,通过key加载value。
/**
* 同步加载
* @param key
* @return
*/
public Object syncOperator(String key){
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(k -> setValue(key).apply(key));
return cache.get(key);
}
public Function<String, Object> setValue(String key){
return t -> key + "value";
}
- 异步加载
AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture。异步加载缓存使用了响应式编程模型。
如果要以同步方式调用时,应提供CacheLoader。要以异步表示时,应该提供一个AsyncCacheLoader,并返回一个CompletableFuture。
/**
* 异步加载
*
* @param key
* @return
*/
public Object asyncOperator(String key){
AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.buildAsync(k -> setAsyncValue(key).get());
return cache.get(key);
}
public CompletableFuture<Object> setAsyncValue(String key){
return CompletableFuture.supplyAsync(() -> {
return key + "value";
});
}
回收策略
Caffeine提供了3种回收策略:基于大小回收,基于时间回收,基于引用回收。
- 基于大小的过期方式
基于大小的回收策略有两种方式:一种是基于缓存大小,一种是基于权重。
maximumWeight与maximumSize不可以同时使用。
// 根据缓存的计数进行驱逐
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10000)
.build(key -> function(key));
// 根据缓存的权重来进行驱逐(权重只是用于确定缓存大小,不会用于决定该缓存是否被驱逐)
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
.maximumWeight(10000)
.weigher(key -> function1(key))
.build(key -> function(key));
基于时间的过期方式
// 基于固定的到期策略进行退出
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> function(key));
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> function(key));
// 基于不同的到期策略进行退出
LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
.expireAfter(new Expiry<String, Object>() {
@Override
public long expireAfterCreate(String key, Object value, long currentTime) {
return TimeUnit.SECONDS.toNanos(seconds);
}
@Override
public long expireAfterUpdate(@Nonnull String s, @Nonnull Object o, long l, long l1) {
return 0;
}
@Override
public long expireAfterRead(@Nonnull String s, @Nonnull Object o, long l, long l1) {
return 0;
}
}).build(key -> function(key));