在项目中使用了caffeine,本文将会介绍其工具的原理
1.caffenine的缓存淘汰策略
Window-TinyLFU
1.新增缓存数据首先写入 Window Cache 区域。当 Window Cache 空间满时,LRU 算法发挥作用,最久未被访问的缓存项会被移出 Window Cache 。这个被移出的缓存项称为 Candidate (候选项),将面临 2 种情况:
如果 Probation Cache 未满,从 Window Cache 中移出的缓存项会直接写入 Probation Cache 中;
如果 Probation Cache 已满,则进入到 TinyLFU 过滤器,与 Protected Cache 移出的缓存项比较。根据 TinyLFU 算法确定该缓存项是淘汰还是写入 Probation Cache。
2.Probation Cache 中的缓存项的访问频率达到一定次数后,就会晋升到 Protected Cache 中。(Ps. 到 Protected 保护区的都是经过 2 层选拔的真热门数据)如果 Protected Cache 也满了,根据 LRU 算法,最久未被访问的缓存项会被移出 Protected Cache ,降级成为 Candidate ,进入到 TinyLFU 过滤器,与 Window Cache 移出的缓存项作比较,根据 TinyLFU 算法确定该缓存项是淘汰还是写入到 Probation Cache 中。
3.此时把目光聚焦到 TinyLFU 过滤器,它汇聚了来自 Window Cache 、Protected Cache 和 Probation Cache 3 个区域中被移出的缓存项。其中,从 Window Cache 和 Protected Cache 中移出的缓存项称为 Candidate ;而从 Probation Cache 中移出的缓存项称为 Victim (受害者) 。这 3 个区域移出的缓存项都是各自区域中最久未被使用的数据,在 TinyLFU 过滤器中进行竞争,竞争算法如下:
如果 Candidate 的访问频率 > Victim 的访问频率,则直接淘汰 Victim ;
如果 Candidate 的访问频率 <= Victim 的访问频率,此时分为两种情况:
如果 Candidate 的访问频率 < 5 ,则淘汰 Candidate ;
如果 Candidate 的访问频率 >= 5 ,则在 Candidate 和 Victim 中随机淘汰一个。
获胜者被称为 Winner ,就会被写入到 Probation Cache 中。
从上面的 W-TinyLFU 的原理描述可知,Caffeine 综合了 LFU 和 LRU 的优势,将不同特性的缓存项存入不同的缓存区域,最近刚产生的缓存项进入 Window Cache 区,不会被淘汰;访问频率高的缓存项进入 Protected Cache 区,也不会淘汰;介于这两者之间的缓存项存在 Probation 区,当缓存空间满了时,Probation 区的缓存项会根据访问频率判断是保留还是淘汰。
通过这种机制,很好的平衡了访问频率和访问时间新鲜程度两个维度因素,尽量将新鲜的缓存项和访问频率高的缓存项都保留在缓存中。同时在维护缓存项访问频率时,引入计数器饱和和衰减 (Count-Min Sketch, 下面会说) 机制,即节省了存储资源,也能较好的处理稀疏流量、短时超热点流量等传统 LRU 和 LFU 无法很好处理的场景。
W-TinyLFU
算法
W-TinyLFU
算法使用 Count-Min Sketch 算法存储访问频率,极大地节省了空间。
看到一个通俗解释:如果需要记录一个值,那我们需要通过多种 Hash 算法对其进行处理,然后在对应的 Hash 算法的记录中 +1。那为什么需要多种 Hash 算法呢?由于这是一个压缩算法必定会出现冲突,比如我们建立一个 byte 的数组,通过计算出每个数据的 Hash 的位置。比如张三和李四,他们两有可能 Hash 值都是相同,比如都是 1 。那 byte[1] 这个位置就会增加相应的频率,张三访问 1 万次,李四访问 1 次,那 byte[1] 这个位置就是 1万零1 ,如果取李四的访问评率的时候就会取出是 1万零1 ,但是李四命名只访问了1次啊!为了解决这个问题,所以用了多个 Hash 算法,可以理解为 long[][] 二维数组的一个概念。比如在第一个 Hash 算法中,张三和李四冲突了,但是在第二个,第三个 Hash 算法中很大的概率不冲突,比如一个算法大概有 1% 的概率冲突,那四个算法一起冲突的概率是 1% 的四次方。通过这个模式我们取李四的访问率的时候取所有算法中,李四访问最低频率的次数。所以他的名字叫 Count-Min Sketch 。
2.caffeine的数据结构
final ConcurrentHashMap<Object, Node<K, V>> data;
写入数据:
将节点添加到策略和数据存储中。如果找到现有节点,则在允许的情况下更新其值
根据不同的更新策略,计算缓存过期时间后数据塞入map中。数据写入后,增加定时任务。注意此时当未自定义线程池时,将会使用默认的forkjoinpool中的线程池,默认线程池可能会影响主程序的执行效率。
@NonNull
Executor getExecutor() {
return (executor == null) ? ForkJoinPool.commonPool() : executor;
}
淘汰数据:
操作ConcurrentHashMap类型的data数据,