Redis 内存淘汰策略,从根儿上理解

2022-09-22 15:10:22 浏览数 (1)

◆ 前言

本文参考源码版本 redis6.2

Redis 基于内存设计,所有数据存放在内存,随着时间推移,内存占用也越来也高 ...

由于内存容量这个物理限制,我们需要在内存使用量达到一定比例后,做一些内存清理工作,以保证有足够的空间来完成正常的处理。

在 Redis 中,完成这个工作的就是本文的主角 ------- Redis 内存淘汰机制。

  • 一定比例:在 redis 中就是 maxmemory 阈值
  • 淘汰策略:在 redis 中目前有两种流行的算法:LRU 与 LFU 算法

如果让你来设计一款内存淘汰策略,你会考虑哪些方面?

1)首先,从用户体验上看:

  • 不要影响服务正常使用,实现平滑淘汰
  • 用户透明,用户不需要做任何额外操作

2)其次,从系统层面来看:

  • 何时清理?
  • 清理哪些?
  • 清理多少?
  • 怎样清理?
  • 清理频次?

带着这些问题,我们一起来探索下,Redis 是如何考虑并实现的。

◆ 一、淘汰策略

淘汰策略也是可以在 redis.config 中指定:

代码语言:javascript复制
maxmemory-policy ...

截止目前,共有 8 种淘汰策略,如下:

◆ 1. 全局淘汰:

1)allkeys-lru:淘汰范围是所有 keys,淘汰最久未使用的 key

2)allkeys-lfu:淘汰范围是所有 keys,淘汰使用频次最少的

3)allkeys-random:淘汰范围:所有 keys,随机淘汰 key

◆ 2. 淘汰 expire :

1)volatile-lru:淘汰范围:所有设置了 expire 时间的 keys,淘汰最久未使用的 key

2)volatile-lfu:淘汰范围:所有设置了 expire 时间的 keys,淘汰使用频次最少的 key

3)volatile-random:淘汰范围:所有设置了 expire 时间的 keys,随机淘汰 key

4)volatile-ttl:淘汰范围:所有设置了 expire 时间的 keys,淘汰 ttl 剩余时间最少的 key

◆ 3. 不淘汰:

1)noeviction:不淘汰,意味着达到限制时,将无法存储

◆ 二、淘汰算法

目前有两种常用的内存淘汰算法,分别致力于从 访问时间 和 访问频次 上解决内存问题。

◆ 1. LRU 算法

Least Recently Used,最近未使用淘汰算法,是一种按照访问时间进行淘汰的算法。

假如只从设计上考虑,我们一般会定义一个队列来存储访问记录,然后每次从队列末尾删除元素即可。

  • 队首新增:访问记录不存在,则在队列尾部新增访问 key
  • 元素位置调整:访问记录存在,则调整元素 key 至队首
  • 队尾删除:当需要淘汰 key 时,从队尾开始判断即可。

但是在 redis 中,内存、CPU 是稀缺物,要尽可能减少内存使用量、CPU 的消耗,因此,在实现上也就更加放松。

redis 采用近似 LRU 算法,每次随机选择一个样本集,并从样本集中挑选最长时间未使用的 key 淘汰。这样一来就不用维护访问队列,而是选择一个合适的样本集大小,也能达到近似 LRU 算法效果。

我们来看看官方测试效果:

  • 浅灰色代表被淘汰的数据
  • 深灰色代表没有被淘汰的数据
  • 绿色代表新增数据

在 redis3.0 之后,当选择样本集 maxmemory-samples = 10,其效果基本已经接近 LRU 算法,但是会更消耗 CPU。

redis.config 中的 maxmemory-samples 默认值为 5,一般情况下应该够用了。

接着,我们来看看 LRU 算法的工作原理,以及存在哪些问题:

代码语言:javascript复制
~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|

其中,~ 字符表示 1 秒,| 表示结束,A 每 5 秒访问一次,B 每两秒访问一次,C、D 每 10 秒访问一次。

我们先看 A、B、C,由于 B 访问频次最高,你会看到 B 几乎总是处于访问队列队首,C 频次最低,大概会出现在访问队列队尾。

不同的是 D,和 C 一样的频率,目前却出现在了队首。

LRU 算法认为,最近访问过的 key,再次访问的概率比较大,而很久没有访问的 key 再次访问的概率比较小(队尾),因此队尾可以优先淘汰。

但是,从以上案例,D 的情况也可以看出,会存在一些极端情况。不过,LRU 总的来说,是比较 OK 的。

每一个 key 在 redis 中都是以 redisObject 形式存在,其中 lru 字段便是记录访问时间:

代码语言:javascript复制
#define LRU_BITS 24

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

◆ 2. LFU 算法

Least Frequently Used,即 最近最少使用淘汰策略,是一种按照访问频次处理的淘汰算法。相对于 LRU 算法,在某些方面效果可能更佳。

LRU 算法有个缺点是,临时数据可能会取代真正经常使用的数据。比如,短时间内,大量临时数据涌入 redis,而触发发生内存淘汰,可能会将那些真正经常使用的数据驱逐。

LFU 是一种按频次处理的淘汰算法,可以很好解决临时数据问题。

假如只从设计上考虑 LFU,一般情况下:要记录每个 key 的使用次数 统计窗口:

  • int 存储使用次数:4 字节,基本满足需求
  • 统计窗口:当窗口滑动时,要累加新的次数,同时也要减去过期数据;本质来说,就是要记录访问明细,看起来挺繁琐。

前面说了,redis 内存和 CPU 都是稀缺品,可以猜测下,不会采用上述方案。

redis LFU 也是采用给一个近似算法:

  • 计数器:使用次数,仅采用 8 byte 存储,最大值 255
  • 衰减时间:不使用滑动窗口,采用衰减时间,达到一定条件使计数器减小。

对应 redis.config 的配置是:

代码语言:javascript复制
lfu-log-factor 10
lfu-decay-time 1

计算规则是这样的:

  • 随机数 R,取数范围:0 ~ 1
  • 概率参数 P = 1 / (old_value * lfu_log_factor 1),这里 old_value 就是计数器的值。
  • 计数器累加:条件是 R < P

从这个公式可以看出,影响累加器的有两个变量:

  • old_value:当前计数值,随着计数值的累加,参数 P 就会越小,也就是说越往后累计越困难,这就确保了计数器不会快速膨胀至 255。
  • lfu_log_factor:在 redis.conf 配置的参数,用于控制累加速度。

我来来看看官方给出的参数 lfu_log_factor,控制累加速度效果:

代码语言:javascript复制
 -------- ------------ ------------ ------------ ------------ ------------ 
| 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        |
 -------- ------------ ------------ ------------ ------------ ------------ 

可以看到,随着 lfu_log_factor 值的变大,计数器累加的速度越来越慢 ...

redis 为了节省内存空间,LFU 的记录仍然复用了 24 bit 的 LRU 字段:

代码语言:javascript复制
           16 bits      8 bits
       ---------------- -------- 
        Last decr time | LOG_C  |
       ---------------- -------- 
  • 高 16 bits 用来记录时间
  • 低 8 bits 用来记录累加数值

计数器累加相关逻辑:

代码语言:javascript复制
  uint8_t LFULogIncr(uint8_t counter) {
      if (counter == 255) return 255;
      double r = (double)rand()/RAND_MAX;
      double baseval = counter - LFU_INIT_VAL;
      if (baseval < 0) baseval = 0;
      double p = 1.0/(baseval*server.lfu_log_factor 1);
      if (r < p) counter  ;
      return counter;
  }

◆ 三、淘汰

前面我们已经讨论了如何淘汰策略以及淘汰算法,这部分我们将讨论真正的淘汰过程:

  • 何时清理?
  • 清理哪些?
  • 清理多少?
  • 怎样清理?

我画了张淘汰的整体流程图,你可以参考下:

◆ 1. 何时清理?

这也是一种惰性思想,一个用户请求过来了,顺便检查下内存使用情况,当 redis 内存使用量达到预定阈值 maxmemory 时,便会触发淘汰机制。

  • 每一条客户端的请求处理之后,看是否有必要进行内存淘汰。如果需要,走淘汰逻辑。

淘汰时也分两种情况:

  • 淘汰数据少:这种很理想,一次性可以搞定。
  • 淘汰数据多:如果数据过多,为避免长时间阻塞,提供了一些可配置的限制,如果达到限制条件还没有清理完成,暂时放入到时间事件中,等待下一轮清理。

◆ 2. 清理哪些?

这个你基本也清楚了,哪些数据需要被清理,主要是看你选择的淘汰策略,以及淘汰算法。

从淘汰策略来看:

  • allkeys- ... 策略,就是全局范围内淘汰。
  • volatile-... 则是在设置了过期 key 范围里淘汰。

从淘汰算法来看:

  • 如果选择 LRU,一般就是先淘汰访问记录末端的数据
  • 而选择 LFU 则淘汰最近最少访问的数据

◆ 3. 清理多少?

待清理的大小 = used - maxmemory,即当前使用内存大小 - 设定的阈值。

一次性没处理完(可以设置一定时长限制),可以扔到时间事件中,周期性的处理,直到达到目标 ...

◆ 4. 怎样清理?

这就是两种淘汰算法的工作了。LRU 和 LFU 在 redis 中都是近似算法,本质是为了减少内存和 CPU 的消耗。

  • 随机选出一个样本集
  • 从样本集选出一个最合适淘汰的 key,这里通过 LRU 或者 LFU 算法选择
  • 清理选择的 key,如果有必要可以使用惰性删除

来源:

https://juejin.cn/post/7142851076293656583

“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com

来都来了,走啥走,留个言呗~

 IT大咖说  |  关于版权

由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!

感谢您对IT大咖说的热心支持!

  • 相关推荐 推荐文章
  • 这个牛逼了,基于(SpringBoot VUE)实现的自定义拖拽式智能大屏
  • 终于有人把怎么搭建数据指标体系给讲明白了,数据分析师必备
  • SpringBoot企业级技术中台微服务架构与服务能力开发平台
  • SQLSERVER backup 命令总结
  • MyBatisPlus又在搞事了!一个依赖轻松搞定权限问题!堪称神器
  • 领导不懂IT技术,分不清报表和BI,看完这篇文章就懂了
  • MIT开源协议,一款百分百开源、支持商用的亚马逊ERP系统
  • 从架构师的角度带你把“响应式编程”给一次性搞明白,果然绝绝子
  • 替代ELK:ClickHouse Kafka FlieBeat才是最绝的
  • RedisJson 横空出世,比 ES 快7 倍,惊爆了
  • 开源文件备份同步神器-DSynchronize

0 人点赞