Redis的缓存淘汰策略

前言

本文主要强调Redis缓存淘汰策略的常见处理方案以及Redis缓存的过期机制,方便自己复习查阅。

一、过期策略

Redis的过期策略通过以下两种方法共同实现:

① 定期删除:

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 过期了,就立即进行删除,不返回任何东西。

总结:定期删除是集中处理,惰性删除是零散处理。

二、淘汰策略

有了以上过期策略的说明后,就很容易理解为什么需要淘汰策略了,因为不管是定期删除还是惰性删除都不是全面的删除,也就是说还会存在一部分已过期的 key 没有被删除掉,所以就需要内存淘汰策略进行补充。

Redis中共有如下多种内存淘汰策略:

  1. noeviction:默认策略,不淘汰任何数据,如果内存达到了设定的值,再添加数据时会报错。

  2. allkeys-lru:加入键的时候,如果过限,首先通过LRU算法选取最近最少使用的数据抛弃。

  3. volatile-lru:加入键的时候如果过限,首先在设置了过期时间的所有键中,选取最近最少使用的数据抛弃。

  4. allkeys-random:加入键的时候如果过限,从所有 key 中随机抛弃。

  5. volatile-random:加入键的时候如果过限,首先在设置了过期时间的所有 key 的集合中随机抛弃。

  6. volatile-ttl:从设置了过期时间的所有 key 的集合中驱逐存活时间最短的数据。

  7. volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的 key 。

  8. allkeys-lfu:从所有键中驱逐使用频率最少的 key 。

2.1 LRU(Least Recently Used)

标准LRU的实现方式:
111
1)新增 key value 的时候首先在链表结尾添加 Node 节点,如果超过 LRU 设置的阈值就淘汰队头的节点以及 HashMap 中对应的节点。

2)修改 key 对应的值的时候先修改对应的 Node 中的值,然后把 Node 节点移到队尾。

3)访问 key 对应的值的时候把访问的 Node 节点移动到队尾即可。

2.2 Redis的LRU实现

Redis 维护了一个 24 位的时钟,可以简单理解为当前系统的时间戳,每隔一定时间会更新这个时钟。每个 key 对象内部同样维护了一个 24 位的时钟,当新增 key 对象的时候会把系统的时钟赋值到这个内部对象时钟。比如我现在要进行 LRU ,那么首先拿到当前的全局时钟,然后再找到内部时钟与全局时钟差最大的 key 进行淘汰,这里值得注意的是全局时钟只有 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 基本持平。
222

2.3 为什么要使用近似LRU

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

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

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

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

2.4 LFU(Least Frequently Used)

LFU 是在 Redis4.0 后出现的, LRU 的最近最少使用实际上并不精确,考虑下面的情况:

A ---- A ---- A ---- A ---- A ---- A ---- A ---- A ---- A ---- A ---- |

B -------------- B --------------- B -------------- B -------------- B|

如果在 | 处使用 LRU 删除,那么 A 距离的时间最久,最应该被删除。但实际上 A 的使用频率要比 B 频繁,所以合理的淘汰策略应该是淘汰 B 。 LFU 就是为应对这种情况而生的。

LFU 把原来的 key 对象的内部时钟的 24 位分成两部分,前 16 位还是代表时钟,后 8 位则代表一个计数器。 16 位的情况下如果还按照秒为单位就会导致不够用,所以一般这里以时钟为单位。而后8位表示当前 key 对象的访问频率, 8 位只能代表 255 ,但是 redis 并没有采用线性上升的方式,而是通过一个复杂的公式,通过配置如下两个参数来调整数据的递增速度。

lfu-log-factor 可以调整计数器 counter 的增长速度, lfu-log-factor 越大, counter 增长的越慢。

lfu-decay-time 是一个以分钟为单位的数值,可以调整 counter 的减少速度。

所以这两个因素就对应到了 LFU 的 Counter 减少策略和增长策略,它们实现逻辑分别如下:

① 降低LFUDecrAndReturn:

1)先从高 16 位获取最近的降低时间 ldt 以及低 8 位的计数器 counter 值;

2)计算当前时间 now 与 ldt 的差值(now-ldt),当 ldt 大于 now 时,那说明是过了一个周期,按照 65535 - ldt + now 计算( 16 位一个周期最大 65535 );

3)使用第 2 步计算的差值除以 lfu_decay_time ,即 LFUTimeElapsed(ldt) / server.lfu_decay_time ,若已过去 n 个 lfu_decay_time ,则将 counter 减少 n 。

② 增长LFULogIncr:

1)获取 0-1 的随机数 r ;

2)计算 0-1 之间的控制因子 p,它的计算逻辑如下:

//LFU_INIT_VAL默认为5
baseval = counter - LFU_INIT_VAL;
//计算控制因子
p = 1.0/(baseval*lfu_log_factor+1);

3)如果 r 小于 p ,counter 增长 1 。

p 取决于当前 counter 值与 lfu_log_factor 因子,counter 值与 lfu_log_factor 因子越大, p 越小, r 小于 p 的概率也越小,counter 增长的概率也就越小。增长情况如下图:
333
从左到右表示 key 的命中次数,从上到下表示影响因子,在影响因子为 100 的条件下,经过 10M 次命中才能把后 8 位值加满到 255 。

③ 新生 KEY 策略:

另外一个问题是,当创建新对象的时候,对象的 counter 如果为 0 ,很容易就会被淘汰掉,还需要为新生 key 设置一个初始 counter 。counter 会被初始化为LFU_INIT_VAL ,默认值为 5 。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值