地球上目前最好的本地缓存框架:Caffeine

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));

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菠萝-琪琪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值