caffeine与guava cache

1.Caffeine 对比 Guava cache
在这里插入图片描述

Caffeine在读写上都明显优于Guava cache,主要是因为2个原因, 淘汰策略 W-TinyLRU 和 Ringbuffer队列.

package Demo;

import com.github.benmanes.caffeine.cache.*;
import org.jetbrains.annotations.NotNull;

import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author fangpengx.zhou
 * @Date 2021/5/28 15:59
 */
public class CaffeineDemo {

    private LoadingCache<String, String> caffeine = Caffeine.newBuilder()
            //自定义时间计时器
            .expireAfter(new Expiry<String, String>() {

                @Override
                public long expireAfterCreate(String key, String value, long currentTime) {
                    return 0;
                }

                @Override
                public long expireAfterUpdate(String key, String value, long currentTime, long currentDuration) {
                    return 0;
                }

                @Override
                public long expireAfterRead(String key, String value, long currentTime, long currentDuration) {
                    return 0;
                }
            })
            //上次写入后开始计时
            .expireAfterWrite(10, TimeUnit.SECONDS)
            //上次访问后开始计时(读写)
            .expireAfterAccess(10, TimeUnit.SECONDS)
            //写入后就会刷新新值(expireAfterWrite 同步, refreshAfterWriter 异步(返回旧值))
            .refreshAfterWrite(10, TimeUnit.SECONDS)
            //最大大小
            .maximumSize(2)
            //key 弱引用 -> 内存溢出前回收
            .weakKeys()
            //value 弱引用 -> 内存溢出前回收
            .weakValues()
            //软引用 -> 下次GC时回收
            .softValues()
            .build(new CacheLoader<String, String>() {

                //同步填充
                @Override
                public String load(@NotNull String key) throws Exception {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return "load" + key;
                }

                //批量填充
                @NotNull
                @Override
                public Map<String, String> loadAll(@NotNull Iterable<? extends String> keys) throws Exception {
                    return null;
                }

                //重新加载
                @Override
                public String reload(@NotNull String key, @NotNull String oldValue) throws Exception {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return "reload" + key;
                }

            });

    private AsyncLoadingCache<String, String> asyncCaffeine = Caffeine.newBuilder()
            .executor(Executors.newSingleThreadExecutor())
            .buildAsync(new AsyncCacheLoader<String, String>() {

                @NotNull
                @Override
                public CompletableFuture<String> asyncLoad(@NotNull String key, @NotNull Executor executor) {
                    return CompletableFuture.supplyAsync(() -> {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        return "asyncLoad" + key;
                    });
                }

                @NotNull
                @Override
                public CompletableFuture<String> asyncReload(@NotNull String key, @NotNull String oldValue, @NotNull Executor executor) {
                    return CompletableFuture.supplyAsync(() -> {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        return "asyncReload" + key;
                    });
                }

            });
    
}


2.W-TinyLRU
2.1主流的淘汰策略
FIFO 先进先出, 队列的形式,淘汰最先进入的缓冲数据,命中率较低
LFU 最近最少未使用, 数据放入队尾,直接淘汰队首的数据即可,但是如果一个热点数据在短时间内没有访问就会导致被淘汰.
LRU 最近最少频率, 对数据进行了访问频率的计算,通过每个数据的频率进行淘汰,这样也避免了LFU的问题.但是LRU也有局限性的,不能随着时间的变化而变化,如果一个热点数据突然不热点了,那这个数据在一段时间内都不能被淘汰出去.
淘汰策略的命中率 FIFO<LFU<LRU,但是对于的成本也是FIFO<LFU<LRU.

在这里插入图片描述

2.2淘汰策略问题
LRU需要记录每一个数据的访问频率,就算数据没有在缓存中也需要记录,这就是一个很大开销
LRU不能随着时间做出改变的
2.3解决方案
2.3.1 Count-Min Sketch:针对开销较大

布隆过滤器:

布隆过滤器通常用于大量数据的匹配(黑名单,白名单),缓存穿透等.用更小的空间实现匹配功能,但是不可避免的会有误差,并且对于删除值不友好.

布隆过滤器通过计算n个hash,并在对应的点进行描黑操作. 查询时也计算对应hash并判断点是否全未黑.

count-min sketch:

count-min sketchs是Bloom Filter(布隆过滤器)的一个变种,key通过n个hash计算定位在对应的位置,取值时在多个位置中取值最低的值.

count-min sketchs使用long型数组,一个long占64位,而存储频率最高为15,转换为二进制为1111.数组一位可以放16种算法,但是sketchs只放了4种hash算法,并分成了4份.这样数据存储128字节就可以存储128*4个数据.

频率最高只能存15,值较小.所以sketch在有值到达了15时会将所有值除2,除2后,继续增加
在这里插入图片描述

2.3.2 LRU+LFU结合: 保证时间变化
Eden队列:在caffeine中规定只能为缓存容量的1%,如果size=100,那这个队列的有效大小就等于1。这个队列中记录的是新到的数据,防止突发流量由于之前没有访问频率,而导致被淘汰,eden区,最舒服最安逸的区域,在这里很难被其他数据淘汰。
Probation队列:叫做缓刑队列,在这个队列就代表你的数据相对比较冷,马上就要被淘汰了。这个有效大小为size减去eden减去protected。
Protected队列:在这个队列中,可以稍微放心一下了,你暂时不会被淘汰,但是别急,如果Probation队列没有数据了或者Protected数据满了,你也将会被面临淘汰的尴尬局面。当然想要变成这个队列,需要把Probation访问一次之后,就会提升为Protected队列。这个有效大小为(size减去eden) * 80% 如果size =100,就会是79。
在这里插入图片描述

所有的新数据都会进入Eden。
Eden满了,淘汰进入Probation。
如果在Probation中访问了其中某个数据,则这个数据升级为Protected。
如果Protected满了又会继续降级为Probation。
队列已满的情况下,会在probation队首选出一个元素(LRU)作为受害者,受害者不会直接淘汰会于攻击者进行pk,攻击者选择队尾的元素.然后在从攻击者和受害者中淘汰一个.

通过我们的Count-Min Sketch中的记录的频率数据有以下几个判断:

如果攻击者大于受害者,那么受害者就直接被淘汰。
如果攻击者<=5,那么直接淘汰攻击者。这个逻辑在他的注释中有解释: 他认为设置一个预热的门槛会让整体命中率更高。
其他情况,随机淘汰。
3.读写效率
读写效率的提升主要原因是, Guava cache在读写操作中会夹杂着其他操作,例如put操作的时候会有过期时间动作.而caffeine在处理其他操作的时候是异步的进行,把任务提交到一个异步的队列中(Ringbuffer).

缓存都是读多写少的场景,所以caffeine在写中多个线程使用同一个ringbuffer,而读是在每个线程都分配一个ringbuffer.

3.1RingBuffer
Ringbuffer是一个无锁的环形数组队列

主流的队列主要为以下几个特征

有界,无界,
加锁,无锁,
链表,数组,堆
在性能上 无锁 > 有锁 , 数组 > 链表. 有界队列必须为有锁… 堆一般使用在带优先级的队列中.

Ringbuffer是一个环形的无界队列(long最大值),无锁的实现(CAS),使用数组的方式实现.

消除伪共享, 数组数据结构, 数组是内存连续的能最大的利用cpu的一级二级三级缓存.因为cpu每次加载缓存是以缓存行的方式加载数据,如果数据连续的话一次读取缓存就可以获取多个有效数据, (空间换时间) 并且 可以预先分配好内存,避免垃圾回收

/**
 * 考虑一般缓存行大小是64字节,一个 long 类型占8字节
 */
static long[][] arr;

@org.junit.Test
public void test3() {

    //填充arr数组
    arr = new long[1024 * 1024][];
    for (int i = 0; i < 1024 * 1024; i++) {
        arr[i] = new long[8];
        for (int j = 0; j < 8; j++) {
            arr[i][j] = 0L;
        }
    }
    long sum = 0L;
    long marked = System.currentTimeMillis();
    //先遍历1024
    for (int i = 0; i < 1024 * 1024; i += 1) {
        //遍历连续的数组
        for (int j = 0; j < 8; j++) {
            sum = arr[i][j];
        }
    }
    System.out.println("Loop times:" + (System.currentTimeMillis() - marked) + "ms");

    marked = System.currentTimeMillis();
    for (int i = 0; i < 8; i += 1) {
        for (int j = 0; j < 1024 * 1024; j++) {
            sum = arr[j][i];
        }
    }
    System.out.println("Loop times:" + (System.currentTimeMillis() - marked) + "ms");

}


Loop times:15ms
Loop times:72ms

数组大小为2^n,使用位运算,最高效的利用cpu
无锁设计,每次读写会提前在对应的数组中申请位置,申请成功在进行读写.
相较与链表 不使用尾指针(取多个消费者中最小的值), 不删除数据
多线程读写:
在多线程的情况下,每个线程获取不同的一段数组空间进行操作。这个通过CAS很容易达到。只需要在分配元素的时候,通过CAS判断一下这段空间是否已经分配出去即可。

读:

在这里插入图片描述

写:

Writer1被分配了下标3到下表5的空间,Writer2被分配了下标6到下标9的空间

在这里插入图片描述

Ringbuffer重叠问题:
在这里插入图片描述
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值