关于缓存的几种算法
关于缓存的设计在架构领域有非常多种类型,常见的缓存算法有FIFO,LRU,LFU,以及现在比较常用的W-TinyLFU算法。
FIFO 算法
这类算法通常会被应用于缓存队列中,当一个查询请求命中了某个元素之后,便会将它放入到队列中,后续的命中元素也是以此类推,直到队列满了之后,老的元素就会被弹出清除。具体流程如下图所示:
不足点:老元素如果某段时间没有访问就会被放置到队列尾部,即使重新访问也依然在队列尾部,当元素面临淘汰的时候,老元素(即使是热点元素)会被误删。
LRU算法
当缓存队列内部已经存在了一批元素之后,后期请求如果命中了队列中的某个元素,那么这个元素就会被前置到队列的头部,从而降低后期被清空的风险。
不足点:LRU算法存在着“缓存污染”的情况需要避免,当突然有一批非热点元素查询打入,大量的非热点数据就会被加载到缓存队列中,从而把真正的热点元素给“挤出去”。
所以为了避免这类缓存污染的问题,后来又出现了一种LFU的策略。
LFU算法
LFU策略就是会在实现缓存队列的基础上额外新增一个内存空间用于记录缓存中每个元素的访问次数,然后根据访问频率来判定哪些元素可以保留,哪些元素需要被删除。
这类算法存在一个很大的弊端,就是需要耗费额外的空间来存储每个元素的访问频率,因此随着缓存元素的数目不断增大,计数器的个数也在不断地增大。
不足点
使用LFU算法也会存在某些程度上的“缓存污染”影响,例如当某天搞秒杀活动,突然一批数据被访问了上千万次,但是第二天这批数据就不再访问了,但是又由于之前秒杀活动导致这批数据的访问基数太过高,导致一直无法清空,所以会一直占用着本地缓存的空间。
W-TinyLFU算法
传统LFU一般使用key-value形式来记录每个key的频率,优点是数据结构非常简单,并且能跟缓存本身的数据结构复用,增加一个属性记录频率就行了,它的缺点也比较明显就是频率这个属性会占用很大的空间,但如果改用压缩方式存储频率呢? 频率占用空间肯定可以减少,但会引出另外一个问题:怎么从压缩后的数据里获得对应key的频率呢?
TinyLFU的解决方案是类似位图的方法,将key取hash值获得它的位下标,然后用这个下标来找频率,但位图只有0、1两个值,那频率明显可能会非常大,这要怎么处理呢? 另外使用位图需要预占非常大的空间,这个问题怎么解决呢?
TinyLFU根据最大数据量设置生成一个long数组,然后将频率值保存在其中的四个long的4个bit位中(4个bit位不会大于15),取频率值时则取四个中的最小一个。
Caffeine认为频率大于15已经很高了,是属于热数据,所以它只需要4个bit位来保存,long有8个字节64位,这样可以保存16个频率。取hash值的后左移两位,然后加上hash四次,这样可以利用到16个中的13个,利用率挺高的,或许有更好的算法能将16个都利用到。
SpringBoot内部使用Caffeine案例介绍
首先需要引入pom配置文件:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>
</dependencies>
然后在SpringBoot结构的项目内部的application.properties配置文件中加入以下内容:
spring.cache.cache-names=userCache
spring.cache.caffein