vlambda博客
学习文章列表

字节面试题:讲一下Redis内存淘汰机制,彻底弄懂Redis的内存淘汰策略

阅读本文前,请您先点击上面的“蓝色字体”,再点击关注,这样您就可以每天学习一点新知识,每天都有进步了。


Redis是基于内存存储,常用于数据的缓存,所以Redis提供了对键的过期时间的设置,实现了几种淘汰机制便于适应各种场景。

设置过期时间 

我们可以在设置键时设置expire time,也可以在运行时给存在的键设置剩余的生存时间,不设置则默认为-1,设置为-1时表示永久存储。


Redis清除过期Key的方式

定期删除+惰性删除


定期删除

Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的 key。

Redis 默认会每秒进行十次过期扫描(100ms一次),过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。

  1. 从过期字典中随机 20 个 key;

  2. 删除这 20 个 key 中已经过期的 key;

  3. 如果过期的 key 比率超过 1/4,那就重复步骤 1;

redis默认是每隔 100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载。

惰性删除

所谓惰性策略就是在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除,不会给你返回任何东西。


定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,即当你主动去查过期的key时,如果发现key过期了,就立即进行删除,不返回任何东西.


注意:Redis中过期的key并不会马上删除,因为定期删除可能正好没抽取到它,我们也没有访问它触发惰性删除

Redis内存淘汰机制

思考一下,如果定期删除漏掉了很多过期的key,而我们也没有再去访问它,如果不加处理,很可能导致内存耗尽。


Redis内存淘汰机制

思考一下,如果定期删除漏掉了很多过期的key,而我们也没有再去访问它,如果不加处理,很可能导致内存耗尽。


Redis配置文件中可以设置maxmemory,内存的最大使用量,到达限度时会执行内存淘汰机制。


Redis中的内存淘汰机制:

没有配置时,默认为no-eviction

字节面试题:讲一下Redis内存淘汰机制,彻底弄懂Redis的内存淘汰策略


  • volatile为前缀的策略都是从已过期的数据集中进行淘汰。

  • allkeys为前缀的策略都是面向所有key进行淘汰。

  • LRU(least recently used)最近最少用到的。

  • LFU(Least Frequently Used)最不常用的。

  • 它们的触发条件都是Redis使用的内存达到阈值时。

LRU算法

首先简单介绍一下 LRU 算法:


LRU 全称是 Least Recently Used,即最近最少使用,会将最不常用的数据筛选出来,保留最近频繁使用的数据。


LRU 会把所有数据组成一个链表,链表头部称为 MRU,代表最近最常使用的数据;尾部称为 LRU代表最近最不常使用的数据;


下图是一个简单的例子:

字节面试题:讲一下Redis内存淘汰机制,彻底弄懂Redis的内存淘汰策略


Redis的LRU实现

Redis维护了一个24位时钟,可以简单理解为当前系统的时间戳,每隔一定时间会更新这个时钟。每个key对象内部同样维护了一个24位的时钟,当新增key对象的时候会把系统的时钟赋值到这个内部对象时钟。


比如我现在要进行LRU,那么首先拿到当前的全局时钟,然后再找到内部时钟与全局时钟距离时间最久的(差最大)进行淘汰,这里值得注意的是全局时钟只有24位,按秒为单位来表示才能存储194天,所以可能会出现key的时钟大于全局时钟的情况,如果这种情况出现那么就两个相加而不是相减来求最久的key。

struct redisServer { pid_t pid;  char *configfile;  //全局时钟 unsigned lruclock:LRU_BITS;  ...};typedef struct redisObject { unsigned type:4; unsigned encoding:4; /* key对象内部时钟 */ unsigned lru:LRU_BITS; int refcount; void *ptr;} robj;

Redis中的LRU与常规的LRU实现并不相同,常规LRU会准确的淘汰掉队头的元素,但是Redis的LRU并不维护队列,只是根据配置的策略要么从所有的key中随机选择N个(N可以配置)要么从所有的设置了过期时间的key中选出N个键,然后再从这N个键中选出最久没有使用的一个key进行淘汰。


下图是常规LRU淘汰策略与Redis随机样本取一键淘汰策略的对比,浅灰色表示已经删除的键,深灰色表示没有被删除的键,绿色表示新加入的键,越往上表示键加入的时间越久。从图中可以看出,在redis 3中,设置样本数为10的时候能够很准确的淘汰掉最久没有使用的键,与常规LRU基本持平。


为什么要使用近似LRU?

  1. 性能问题,由于近似LRU算法只是最多随机采样N个key并对其进行排序,如果精准需要对所有key进行排序,这样近似LRU性能更高。

  2. 内存占用问题,redis对内存要求很高,会尽量降低内存使用率,如果是抽样排序可以有效降低内存的占用。

  3. 实际效果基本相等,如果请求符合长尾法则,那么真实LRU与Redis LRU之间表现基本无差异。

  4. 在近似情况下提供可自配置的取样率来提升精准度,例如通过 CONFIG SET maxmemory-samples <count> 指令可以设置取样数,取样数越高越精准,如果你的CPU和内存有足够,可以提高取样数看命中率来探测最佳的采样比例。


LFU算法

LFU 全称 Least Frequently Used,即最不经常使用策略,它是基于数据访问次数来淘汰数据的,在 Redis 4.0 时添加进来。它在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。


前面说到,LRU 使用了 RedisObject 中的 lru 字段记录时间戳,lru 是 24bit 的,LFU 将 lru 拆分为两部分:


ldt 值:lru 字段的前 16bit,表示数据的访问时间戳

counter 值:lru 字段的后 8bit,表示数据的访问次数 使用 LFU 策略淘汰缓存时,会把访问次数最低的数据淘汰,如果访问次数相同,再根据访问的时间,将访问时间戳最小的淘汰。


为什么 Redis 有了 LRU 还需要 LFU 呢?

在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。


由于 LRU 是基于访问时间的,如果系统对大量数据进行单次查询,这些数据的 lru 值就很大,使用 LFU 算法就不容易被淘汰。