Redis最为常用的是拿来做缓存,而Redis之所以这么快的原因之一是搭上了内存那纳秒级别的处理速度来存储数据,极大提升了应用服务的性能。(从用户角度翻译过来就是这玩意反应快了)
但是,但凡技术总有它的局限性,例如在计算机中内存空间远比磁盘空间要小得多,而且内存比磁盘贵。所以我们要是把数据都放内存,显然是一件成本高,性价很低的事情。
所以更多的是采取让Redis存放热数据,从统计上来说,在大部分业务场景中,按二八定律,是20%的数据贡献了的访问量和访问频率可能接近或超过80%(当然总有部分例外)。
但是,内存空间大小就这么多,随着业务缓存数据量不断增多,不可避免就会将有限的内存空间不小心给占满。
那Redis是怎么解决缓存占满内存的?
我们先来看Java,使用Java都知道,Java是运行在JVM上的,而JVM的一大亮点就是拥有不用让C或C++的同学一样去关心内存回收情况,也就是垃圾回收机制。Redis也有自己的内存回收机制,但是相对JVM来说,Redis要"简单"一些,因为Redis内存回收机制主要两个方面的策略。
Redis内存回收机制策略
Redis 删除过期键策略
惰性删除:
顾名思义,惰性删除并不主动做任何操作,而是当客户端读取到设置了超时的键时,如果已经超过过期时间就会删除。但是很明显的问题就是当过期键一直没有访问到而及时删除,那么就会导致不能让内存及时释放。
定时删除:
定时删除实际上就是在Redis内部开启了一个定时任务,通过默认每秒定时的运行多少次和按键过期比例以及快慢的速率模式去回收键。
除了删除过期键策略当然还远不够,所以就进一步通过算法来筛选数据淘汰的淘汰策略。
Redis淘汰策略
但是不管前面的删除过期键策略,还是淘汰策略目的本身都是来防止内存溢出的这一点。Redis淘汰策略提供了8种淘汰策略,Redis4.0实现了6种淘汰策略,4.0之后又增加了2种策略,所以Redis有 8 种淘汰策略。
可以分成两类:
- 不淘汰数据的策略,仅有
noeviction
一种。 - 会淘汰数据的有7种策略。
我们这里主要关注淘汰数据的7种策略,这7种细看可以再次归成两类:
会在所有数据中淘汰的: allkeys-lru
、allkeys-random
、allkeys-lfu
会在设置过期时间数据中淘汰的:volatile-lru
、volatile-random
、volatile-ttl
、volatile-lfu
小伙伴们,我把熬夜整理的思维导图放在这了。
Redis淘汰策略详解
默认情况,当Redis的内存超过maxmemory
时,noeviction
是作为默认策略的,并不会淘汰任何数据。在Redis缓存一旦被占满之后的写请求都不会再处理,会直接的返回错误。
接下来是 allkeys-lru
、allkeys-random
、allkeys-lfu
四种淘汰策略。它们会在设置过期时间的数据中进行淘汰,所以它们筛选的数据范围都在设置了过期键上。当数据过期时,即使缓存没有写满也会被淘汰删除。
volatile-ttl
:根据键的ttl(生存时间值),删除设置过期时间最近的键,先过期的被先删除。
volatile-random
:random也就是随机,设置了过期的 key 会随机的删除。
volatile-lru
:在设置过期时间的 key,使用LRU算法筛选淘汰键。
volatile-lfu
:在设置过期时间的 key,使用LFU算法筛选淘汰键。
allkeys-lru
、allkeys-random
、allkeys-lfu
前缀都带着 all
这三种淘汰策略的淘汰数据范围包括了所有的键值,范围是所有键值就是无论是否设置过期时间都会进行淘汰。
allkeys-random
:在所有键中随机淘汰数据。
allkeys-lru
:在所有键中使用LRU算法筛选数据。
allkeys-lfu
:在所有键中使用lfu算法筛选数据。
不管是 ttl
还是 random
算法规则是比较简单,而主要 lru
和 lfu
算法也不复杂,让我们一起看看。
LRU(Least Recently Used)
LRU (Least Recently Used) 是最近最少使用原则,是将最近最不常用使用的数据进行筛选,最近不常使用的淘汰,最近常使用的数据留在缓存。
具体怎么筛选可以看下面的例子,假设在一块有限的空间里,最近访问会被移到顶端,最近没访问到的会移到末端,也就是LRU端。当空间被占满时,此时刚好有新增的数据时,就会把LRU端的末尾 key
替换淘汰掉。
你看LRU算法是不是很有用户体验 “如果有数据最近被访问过,那么再被访问的几率也会很高”
那如果要去实现LRU算法,自然就需要有支撑它的数据结构,此时就可以使用链表,用链表来存放所有缓存数据。不过只使用链表,会有问题,那就是当面临数据量大的情况,链表的移动也会显得笨拙而带来耗时,进而影响Redis性能。
Redis自然不会放过这个可以优化的机会,所以Redis在LRU算法上动手脚。所以在一开始就记录每个数据最近一次被访问的时间戳。之后当Redis准备淘汰数据时,首先第一次随机的选出N
个数据,然后将其作为候选集合,最后比较这N
个数据携带的lru
字段,最小的会从缓存中被淘汰。
Redis提供 maxmemory-samples
的配置参数,让Redis选出数据作为候选数据集。
当面临淘汰数据,Redis需要挑选数据,那么就会进到首次创建的淘汰候选集合。
挑选标准是:进入候选集合的数据lru
属性值必须小于候选集合中最小的lru
值。
当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了maxmemory-samples
,Redis就把候选数据集中lru字段值最小的数据淘汰出去。
你看这样一来Redis缓存就可以不用为不断增多的数据维护一个也不断增大的大链表,省去每次数据访问都移动链表的开销,缓存的性能就能得到提升。
LFU(Least Frequently Used)
LFU (Least Frequently Used)是最近最少频率使用,楞一看LFU缓存策略跟 LRU很相似,相似就对了,因为LFU就是在LRU的策略基础上优化出来的缓存策略。
LFU不同与LRU的是LFU把LRU原来的24bit
的lru
字段拆分成Idt
值和 counter
值两部分。其中Idt
是lru
字段的前16bit
表示访问时间戳。counter
值是lru
字段的后8bit
也就是表示访问次数。
LFU算法用的是访问频次递增和访问频次衰减两种方式。
访问频次递增
是通过counter
来递增,但是它所能表示的最大值只有255,所以采用了更优的计数方式。每当数据被访问时,计数器值乘以lfu_log_factor
再加1,取其倒数,得到p值;之后p值和取值范围 0 和 1 之间的随机数 r 比大小,当 p 值大于r值,计数器才加 1.
1/(baseval * lfu_log_factor + 1)
Redis官网 提供的一张表,当lfu_log_factor
取不同值,不同访问次数,计数器值的变化情况。
从表中可以看到,lfu_log_factor
取值为1,访问次数100k时, counter
值就到顶255,没法区分访问次数。当lfu_log_factor
取值为100时,访问次数10M,counter
值达到255,此时,访问次数小于10M的不同数据都可以通过counter
值区分出来。
访问频次衰减
Redis实现LFU策略时,除了访问频次递增,还设计了一个衰减机制。因为从上可知,counter
一直递增会到达顶 255,而且纯粹的递增不能反应一个 key
的热度,所以 key
如果一段时间不被访问,counter
也需要对应减少。
递减的速度由 lfu-decay-time
配置项控制 counter
的递减速度,默认值 1表示如果N
分钟没有访问,那么 counter
减 N
。
总结
我们围绕 Redis是怎么解决缓存占满内存展开了Redis的内存回收策略,Redis的内存回收策略有两个方面,删除过期键策略和淘汰策略,但是不管是删除过期键策略还是淘汰策略目的都是来控制防止内存溢出。在淘汰策略中,Redis4.0实现了6种淘汰策略,4.0之后又增加了2种策略,所以Redis一共有8 种淘汰策略。其中最为主要的LRU和LFU算法策略。LFU是在LRU的基础上的策略,但是LFU并不是用来替换LRU;它们各自的数据筛选侧重点不同,前者LRU策略侧重数据时效性,而后者LFU侧重访问频次。
朋友们,到这就接近尾声了。感兴趣的朋友可以在3A服务器上部署环境尝试一下。