目录
- 引出
- Redis过期删除策略
- Redis的两种过期策略:定期删除 + 惰性删除
- 定期删除
- 惰性删除
- Redis两种过期删除策略存在的问题
- Redis缓存淘汰策略
- Redis中的LRU和LFU算法
- 1、LRU(Least Recently Userd最近最少使用)
- LFU 算法的引入
- 2、LFU(least Frequently Userd最近最不频繁使用)
引出
Redis的key达到过期时间,Redis就会马上删除么?
结论是:并不会立马删除
为什么并不会立马删除,这个时候我们就需要说到Redis的数据过期清除策略 与 内存淘汰策略。
在使用Redis时,我们一般会为Redis的缓存空间设置一个大小,不会让数据无限制地放入Redis缓存中。可以使用下面命令来设定缓存的大小,比如设置为4GB:
CONFIG SET maxmemory 4gb
既然 Redis 设置了缓存的容量大小,那缓存被写满就是不可避免的。当缓存被写满时,我们需要考虑下面两个问题:决定淘汰哪些数据,如何处理那些被淘汰的数据。
Redis过期删除策略
如果我们设置了Redis的key-value的过期时间,当缓存中的数据过期之后,Redis就需要将这些数据进行清除,释放占用的内存空间。Redis中主要使用 定期删除 + 惰性删除 两种数据过期清除策略。
Redis的两种过期策略:定期删除 + 惰性删除
定期删除
Redis的定期删除是指:Redis默认每隔0.1s就随机抽取一些设置了过期时间的key,检查这些key是否过期,如果有过期就删除。
注意这里是随机抽取,那为什么要随机抽取呢?我们可以想一想看,如果redis中存了十几万个key,每隔0.1s就遍历所有设置过期时间的key的话,会给CPU带来很大的负担的。
问题:Redis中为什么不直接使用定期删除策略呢?
定时删除,会用一个定时器来负责监视key,过期则将这些key删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求上,而不是删除key上,所以就没有采用这一策略。
惰性删除
定期删除可能导致很多过期的可以到了过期时间并没有被删除掉,这个时候就要使用到惰性删除。
Redis的惰性删除是指:在你获取某个key时,redis会检查一下,这个key是否设置了过期时间并且是否已经过期,是的话就删除。
Redis两种过期删除策略存在的问题
如果某个key过期后,定期删除没有删除成功,然后也没有去请求key,也就是说惰性删除也没有生效。这个时候,如果大量过期的key堆积在内存中,redis的内存会越来越高,导致redis的内存耗尽。这个时候我们就应该采用Redis的缓存淘汰机制了。
Redis缓存淘汰策略
Redis一共提供了八种缓存淘汰策略。如下图
1、noeviction:不进行淘汰数据。一旦缓存被写满,再有写请求进来,Redis就不再提供服务,而是直接返回错误。
2、volatile-ttl:在设置了过期时间的键值对中,移除即将过期的键值对。
3、volatile-random:在设置了过期时间的键值对中,随机移除某个键值对。
4、volatile-lru:在设置了过期时间的键值对中,移除最近最少使用的键值对。
5、volatile-lfu:在设置了过期时间的键值对中,移除最近最不频繁使用的键值对。
6、allkeys-random:在所有键值对中,随机移除某个key。
7、allkeys-lru:在所有的键值对中,移除最近最少使用的键值对。
8、allkeys-lfu:在所有的键值对中,移除最近最不频繁使用的键值对。
这么多Redis缓存淘汰策略,我们该如何选择呢?
通常情况下,推荐优先选择 allkeys-lru 策略。这样可以充分利用LRU这一经典的缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。
如果我们的业务数据中有明显的冷热数据区分,那么建议我们使用 allkeys-lru 策略。
如果我们的业务数据访问频率相差不大,没有明显的冷热数据的区分,那么建议我们使用 allkeys-random 策略,随机选择淘汰的数据就行。
对于那些没有设置过期时间的键值对,那么使用如 volatile-lru , volatile-lfu , volatile-random 和 volatile-ttl 策略的行为和noeviction 基本上是一致的,一旦缓存被写满,再有写请求进来,Redis就不再提供服务,而是直接返回错误。
我们来说说最主要的两个算法:LRU(Least Recently Userd最近最少使用)和LFU(least Frequently Userd最近最不频繁使用)算法
首先我们先说一下他们两个的区别:
LRU(Least Recently Userd):最近最少使用,跟使用的最后一次时间有关,淘汰离现在最久的。
LFU(least Frequently Userd):最近最不频繁使用,跟使用的次数有关,淘汰使用次数最少的。
Redis中的LRU和LFU算法
1、LRU(Least Recently Userd最近最少使用)
LRU算法的全称是 Least Recently Uses,按照最近最少使用的原则来筛选数据,最不常用的数据会被筛选出来。LRU算法会把所有的数据组织成为一个链表,链表的头和尾表示MRU端和LRU端,分别表示最近最常使用和最近最不常使用的数据,我们来看一个例子。
如果有一个新数据45要被写入缓存,但此时已经没有缓存空间了,也就是链表没有空余位置了,那么LRU算法会做两件事情:数据45是刚被访问的,所以它会被放到MRU端;算法把LRU端的数据5从缓存中删除,相应的链表中就没有数据为5的数据了。LRU认为刚刚被访问过的数据,肯定会被再次访问,所以就把它放在MRU端;长久不访问的数据,肯定不会再访问了,所以就让它后移到LRU端,当缓存满时,就优先删除它。
但是LRU算法会出现几个问题:
LRU算法在实际实现时,需要用链表管理所有的缓存数据,移除元素时直接从链表的队尾移除,增加时增加到头部就可以了,但是这会带来额外的空间开销。而且,当有数据被访问时,需要在链表上把数据移动到MRU端,由于是链表,虽然这个开销比较小,但是如果有大量数据被访问,那么就会带来很多链表移动的操作了,而且会减低Redis缓存性能。
所以,Redis并没有直接使用原汁原味的LRU算法,而是对LRU算法做了优化,解决了上面的问题,减少了数据淘汰对缓存性能的影响。具体来说:
1、Redis 默认会记录每个数据的最近异常访问的时间戳(在一个数据结构中 RedisObject 中Iru字段来记录)
2、然后,Redis 在决定淘汰数据时,第一次会随机选出N个数据,把他们作为一个候选集合。接下来,Redis会比较这N个数据的 Iru字段,把 Iru 字段的最小的数据从缓存中淘汰出去。
3、当再次需要淘汰数据时,Redis 需要挑选数据进入之前创建的候选集合,然后再次进行比较,这里的比较标准是:能进入候选集合数据的 Iru 字段值必须小于候选集合中最小的 Iru 值。当有新的数据进入候选集合后,如果候选集合数据的个数已经达到了N个,Redis 就把候选数据集中的 Iru 字段最小的数据淘汰出去。
这样一来,Redis 缓存就不用为所有的数据维护一个大链表,也不用在每次数据访问时都移动链表项,提升了缓存的性能。
同时,Redis 提供了一个日志参数 maxmory-samples ,这个参数就是 Redis 选出的数据个数N。例如,我们执行如下命令,就可以让 Redis 选出100个数据作为候选数据集:
config set maxmemory-samples 100
LFU 算法的引入
LRU对于热点数据来说实际上并不是那么精准,比如下面的情况“|”表示删除,在距离我们删除的时候,如果我们使用LRU算法,遵循最近最少使用,那么在 A 和 D 之中,A会先被删除,但是实际上A的使用频率要比D频繁,所以合理的淘汰策略一个是淘汰D,而LFU最近最不频繁使用算法就是为了应对这种情况而生的。
~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|
2、LFU(least Frequently Userd最近最不频繁使用)
LFU算法是在Redis4.0之后出现的,它的核心思想是根据 key 的最近访问的频率进行淘汰,很少被访问的优先被淘汰,被访问多的则被留下来。LFU算法比起LRU算法来说能更好的标识一个 Key 被访问的热度。我们再举个例子,如果我们使用LRU算法来探测热点数据,一个 key 很久没有被访问到,只是刚刚偶尔被访问到了一次,那么它就认为是热点数据,不会被淘汰,而有些 key 将来可能被访问到的却被淘汰了。如果我们使用了LFU算法则不会出现这种情况,因为使用一次并不会使用过 key 成为热点数据。LRU关注最后一次访问的时间,淘汰离现在最久的。LFU关注使用次数,淘汰使用次数最少的。
但是LFU的实现比LRU更为复杂,它需要考虑几个问题:
- 如果实现为链表,当对象被访问时安装访问次数移动到链表的某个位置可能是低效的,因为可能存在大量访问次数相同的key。
- 某些 key的访问次数可能非常大,理论上可以无限大,单实际上我们并不需要精确的访问次数。
- 访问次数特别大的 key可能之后都不再访问了,但是因为访问次数大而占用着内存不被淘汰,需要一个方法来逐步衰减次数。
奇妙的是 Redis 只用了 24bit 就来记录上述信息,注意是 bit ,其中这 24bit 中,16bit 用于存放上一次的递减时间(解决第三个问题),用剩下的 8bit 来存放访问次数(解决第二个问题)