Redis 缓存淘汰策略以及 LRU、LFU 算法

目录

前言

maxmemory-policy 淘汰策略

Redis 中的 LRU 算法

传统 LRU 算法

Redis LRU 算法

Redis 中的 LFU 算法

策略

实现

相关配置

前言​​​​​​​​​​​​​​


Redis 使用内存来保存数据,而物理内存是有限的,如果不对 Redis 使用内存做出限制,当内存不够用时,操作系统将通过 swap 分区让数据在内存和硬盘之间来回置换,这会严重影响 Redis 性能,因此我们一般要配置 Redis 可以使用的最大内存(maxmemory)。​​​​​​​

maxmemory-policy 淘汰策略


当使用的内存达到设置的最大使用内存时,将会触发内存淘汰策略, Redis 提供以下八种策略:

  • noeviction:不淘汰任何数据,内存溢出时直接返回 OOM 错误信息;
  • volatile-random:随机移除设置了过期时间的 Key;
  • volatile-ttl:针对带有过期时间的 Key,移除离过期时间最近的 Key(越早过期越先移除);
  • volatile-lru:针对带有过期时间的 Key,使用 LRU(Least Recently Used) 算法筛选移除;、
  • volatile-lfu:针对带有过期时间的 Key,使用 LFU(Least Frequently Used) 算法筛选移除;
  • allkeys-random:针对所有 Key 进行随机移除;
  • allkeys-lru:针对所有 Key 使用 LRU(Least Recently Used) 算法移除;
  • allkeys-lfu:针对所有 Key 使用 LFU(Least Frequently Used) 算法移除。

Redis 中的 LRU 算法


传统 LRU 算法

传统的 LRU 算法 把所有的数据组织成一个链表,链表的头和尾分别表示 MRU 端最近最常使用 LRU 端最近最不常用

每次访问数据时,把该数据移动到 MRU 端,当缓存没有空间需要删除数据的时候,从 LRU 端选择数据删除,这样就可以把最近访问的数据留在缓存中,删除离当前时间最久没有被访问的数据。

当然,因为传统的算法需要使用链表来保存所有的数据,同时存在大量的数据移动,因此,Redis 并没有使用这种做法。

Redis LRU 算法


Redis 在每个数据对象 RedisObject 中存放 lru 字段,表示该数据最近一次访问的时间戳,以后做数据淘汰时用该字段作为比较依据。

当执行数据淘汰时, 首次 执行将按以下步骤选择数据:

1、随机 选出 N (maxmemory-samples)个数据,把它们作为一个候选集合;

2、比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据淘汰出去;

以后 再次 进行数据淘汰时,将以 第一次淘汰时创建的候选集合中最小的 lru 值 minLruInSet 为基准,挑选 lru 字段值 小于 minLruInSet 的数据并放入到集合中,当候选数据集中的数据个数再次达到 maxmemory-samples 时,Redis 就把候选集合中 lru 字段值最小的数据淘汰出去。

通过维护这个 lru 小值集合可以减小发生数据淘汰时对 redis 产生的性能影响,因为它不需要使用链表来保存所有的数据,也不存在数据的移动。

官网 表明在样本数 maxmemory-samples = 10 的情况下,Redis3.0 很接近真正的 LRU 实现。

Redis 中的 LFU 算法


策略

LRU 算法存在一个缺陷,因为它 只关心数据的访问时间,在发生 扫描式单次查询操作时,所有的数据都会被访问一次,这样可能导致很多热数据反而被排到了 LRU 的末端而被淘汰。

针对这个问题,LFU 缓存策略 在 LRU 策略基础上,为每个数据 增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时:

1、首先根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出去;

2、如果两个数据的访问次数相同,再比较两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出去。

因此,LFU 算法会将访问更频繁的数据保留,而优先淘汰访问次数最少的数据,在访问次数相当的情况下再选择访问时间最久远的数据淘汰。

实现

LFU 在实现上是把原来 24bit 大小的 lru 字段拆成两部分:

  • ldt 值:lru 字段的前 16bit,表示数据的访问时间戳;
  • counter 值:lru 字段的后 8bit,表示数据的访问次数。

8 bit 记录访问次数,最多只能到 255。如果访问一次就加 1 的话,可能大部分数据都会很快达到这个值,那这个值将失去意义。因此,Redis 使用了一个增长更慢的计数规则:

double r = (double)rand()/RAND_MAX;
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) 
    counter++;
  • r 是一个取值范围在(0,1)间的随机数;
  • baseval 是计数器的当前值;
  • lfu_log_factor 为计数器增长因子。

每当数据被访问时,首先,用 baseval 乘以 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;

然后,把这个 p 值和 r 值作比较, p 值大于 r 值时计数器加 1。

官网也提供了 lfu_log_factor 不同取值时的计数器增长情况:

+--------+------------+------------+------------+------------+------------+
| factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
+--------+------------+------------+------------+------------+------------+
| 0      | 104        | 255        | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 1      | 18         | 49         | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 10     | 10         | 18         | 142        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 100    | 8          | 11         | 49         | 143        | 255        |
+--------+------------+------------+------------+------------+------------+

此外,Redis 还设计了一个 counter 值的衰减机制,这主要是为了能够淘汰那些可能短时间内被频繁访问,但是之后不再需要的数据。

unsigned long LFUDecrAndReturn(robj *o) {
  // 获取lru的高16位,也就是ldt
  unsigned long ldt = o->lru >> 8;  
  // 获取lru的低8位,也就是logc  
  unsigned long counter = o->lru & 255;
  // 根据配置的lfu-decay-time计算Logistic Counter需要衰减的值
  unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
  if (num_periods)
    counter = (num_periods > counter) ? 0 : counter - num_periods;
  return counter;
}

// 计算距离上次访问的间隔时间
unsigned long LFUTimeElapsed(unsigned long ldt) {
    // 取当前时间戳(单位:分钟)
    unsigned long now = LFUGetTimeInMinutes();
    // 计算时间差
    if (now >= ldt) 
        return now-ldt;
    return 65535-ldt+now;
}

// 获取当前时间戳,以分钟为单位,取低 8 位
unsigned long LFUGetTimeInMinutes(void) {
    return (server.unixtime/60) & 65535;
}

LFU 策略使用衰减因子配置项 lfu_decay_time 来控制访问次数的衰减。

因为数据对象只有前 16 位用来保存时间戳,所以只能比较到分钟级别,在计算的时候:

首先把当前时间换成以分为单位,然后计算 其与 ldt 的差值,再把这个差值除以 lfu_decay_time ,所得的结果就是数据 counter 要衰减的值。

相关配置


  • maxmemory :最大使用内存
  • maxmemory-policy :淘汰策略
  • maxmemory-samples: LRU 算法中采样集合大小
  • lfu_log_factor:计数器增长因子
  • lfu-decay-time:LFU 算法计数器衰减因子

以上参数可以通过配置文件 redis.conf 配置,也可以通过命令行 config set [key] [value] 设置。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值