你应该知道的缓存进化史( 四 )


你应该知道的缓存进化史

文章插图
 
 
你应该知道的缓存进化史

文章插图
 
 
这里和以前的做个对比,简单的举个例子:如果一个hashMap来记录这个频率,如果我有100个数据,那这个HashMap就得存储100个这个数据的访问频率 。哪怕我这个缓存的容量是1,因为Lfu的规则我必须全部记录这个100个数据的访问频率 。如果有更多的数据我就有记录更多的 。
在Count-Min Sketch中,我这里直接说caffeine中的实现吧(在FrequencySketch这个类中),如果你的缓存大小是100,他会生成一个long数组大小是和100最接近的2的幂的数,也就是128 。而这个数组将会记录我们的访问频率 。在caffeine中他规则频率最大为15,15的二进制位1111,总共是4位,而Long型是64位 。所以每个Long型可以放16种算法,但是caffeine并没有这么做,只用了四种hash算法,每个Long型被分为四段,每段里面保存的是四个算法的频率 。这样做的好处是可以进一步减少Hash冲突,原先128大小的hash,就变成了128X4 。
一个Long的结构如下:
你应该知道的缓存进化史

文章插图
 
我们的4个段分为A,B,C,D,在后面我也会这么叫它们 。而每个段里面的四个算法我叫他s1,s2,s3,s4 。下面举个例子如果要添加一个访问50的数字频率应该怎么做?我们这里用size=100来举例 。
  1. 首先确定50这个hash是在哪个段里面,通过hash & 3必定能获得小于4的数字,假设hash & 3=0,那就在A段 。
  2. 对50的hash再用其他hash算法再做一次hash,得到long数组的位置 。假设用s1算法得到1,s2算法得到3,s3算法得到4,s4算法得到0 。
  3. 然后在long[1]的A段里面的s1位置进行+1,简称1As1加1,然后在3As2加1,在4As3加1,在0As4加1 。
 
你应该知道的缓存进化史

文章插图
 
 
这个时候有人会质疑频率最大为15的这个是否太小?没关系在这个算法中,比如size等于100,如果他全局提升了1000次就会全局除以2衰减,衰减之后也可以继续增加,这个算法再W-TinyLFU的论文中证明了其可以较好的适应时间段的访问频率 。
6.3、读写性能
在guava cache中我们说过其读写操作中夹杂着过期时间的处理,也就是你在一次Put操作中有可能还会做淘汰操作,所以其读写性能会受到一定影响,可以看上面的图中,caffeine的确在读写操作上面完爆guava cache 。主要是因为在caffeine,对这些事件的操作是通过异步操作,他将事件提交至队列,这里的队列的数据结构是RingBuffer 。然后通过会通过默认的ForkJoinPool.commonPool(),或者自己配置线程池,进行取队列操作,然后在进行后续的淘汰,过期操作 。
当然读写也是有不同的队列,在caffeine中认为缓存读比写多很多,所以对于写操作是所有线程共享一个Ringbuffer 。
你应该知道的缓存进化史

文章插图
 
 
对于读操作比写操作更加频繁,进一步减少竞争,其为每个线程配备了一个RingBuffer:
你应该知道的缓存进化史

文章插图
 
 
6.4、数据淘汰策略
在caffeine所有的数据都在ConcurrentHashMap中,这个和guava cache不同,guava cache是自己实现了个类似ConcurrentHashMap的结构 。在caffeine中有三个记录引用的LRU队列:
  • Eden队列:在caffeine中规定只能为缓存容量的%1,如果size=100,那这个队列的有效大小就等于1 。这个队列中记录的是新到的数据,防止突发流量由于之前没有访问频率,而导致被淘汰 。比如有一部新剧上线,在最开始其实是没有访问频率的,防止上线之后被其他缓存淘汰出去,而加入这个区域 。伊甸区,最舒服最安逸的区域,在这里很难被其他数据淘汰 。
  • Probation队列:叫做缓刑队列,在这个队列就代表你的数据相对比较冷,马上就要被淘汰了 。这个有效大小为size减去eden减去protected 。
  • Protected队列:在这个队列中,可以稍微放心一下了,你暂时不会被淘汰,但是别急,如果Probation队列没有数据了或者Protected数据满了,你也将会被面临淘汰的尴尬局面 。当然想要变成这个队列,需要把Probation访问一次之后,就会提升为Protected队列 。这个有效大小为(size减去eden) X 80% 如果size =100,就会是79 。
这三个队列关系如下:


推荐阅读