Redis内存淘汰策略 学习笔记

59 阅读6分钟

前言

前一篇文章 juejin.cn/post/747162… 说的是过期淘汰策略,删除的是已过期的key,而当Redis的运行内存超过设置的最大内存时,会触发内存淘汰策略,删除符合条件的key,以此保障Redis的高效运行。

在配置文件redis.conf中,可通过参数 maxmemory <bytes> 来设定最大运行内存,不同操作系统,maxmemory的默认值不同

  • 64位操作系统中,maxmemory默认值为0,表示没有内存大小限制,那么不管用户存放多少数据,Redis也不会对可用内存进行检查,直到Redis实例因内存不足而崩溃也无作为
  • 32位操作系统,maxmemory的默认值为3G,因为32位机器最大支持4GB的内存,系统本身就需要一定的内存资源来运行。

内存淘汰策略有哪些?

总共8种,大体上分为「不进行数据淘汰」和「进行数据淘汰」两类策略,针对「进行数据淘汰」这一类,又可细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略

  1. noeviction:Redis3.0之后默认的内存淘汰策略,表示当运行内存超过最大设置后,不淘汰任何数据,这时如果有新的数据写入,报错禁止写入,不淘汰任何数据;而单纯的查询或删除操作不受影响。
  2. volatile-random:在设置了过期时间的数据中,随机淘汰任意key-value
  3. allkeys-random:在所有数据中,随机淘汰任意key-value
  4. volatile-lru:Redis3.0之前默认的内存淘汰策略,在设置了过期时间的数据中,淘汰最久未使用的key-value
  5. allkeys-lru:在所有数据中,淘汰最久未使用的key-value
  6. volatile-lfu:Redis4.0之后新增的内存淘汰策略,在设置了过期时间的数据中,淘汰最少使用的key-value
  7. allkeys-lfu:Redis4.0之后新增的内存淘汰策略,在所有数据中,淘汰最少使用的key-value
  8. volatile-ttl:在设置了过期时间的数据中,优先淘汰过期时间更早的key-value

image.png 可以使用config get maxmemory-policy 命令,查看当前的Redis内存淘汰策略

config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"

修改内存淘汰策略设置有两种方法

  1. 通过config set maxmemory-policy <策略>命令设置,优点是立即生效,不需要重启Redis服务,但是重启后就会失效
  2. 通过修改redis.conf中的maxmemory-policy <策略>,重启Redis才会生效,但配置不会丢失

Redis是如何实现LRU的?

LRU 全称是 Least Recently Used,最近最少使用,选择淘汰最近最少使用的数据,传统LRU算法的实现是基于链表,元素按照操作顺序从前往后排列,最新操作的建会被移动到表头,当需要内存淘汰时,直接删除链表尾部的元素即可,因为该元素就代表最近最少使用的

但是传统LRU算法存在两个问题:

  1. 需要用链表管理所有缓存数据,带来额外的空间开销
  2. 当有数据被访问时,需要把该元素移动到链表头部,如果有大量不同的数据被访问,会产生很多链表移动操作,降低Redis性能

因此,Redis并没有使用这样的实现,而是采用一种近似LRU算法,在Redis的对象结构中添加一个额外的字段,记录此数据的最后一次访问时间,目的是为了更好的节省内存

当Redis进行内存淘汰时,采用随机抽样的方式,随机抽取几个值(可配置),然后淘汰最后一次访问时间最早的那个,即最近最少使用的

但是,LRU算法还有一个问题未得到解决,那就是「缓存污染问题」,例如一次性读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在Redis内存中很长一段时间,造成污染。

因此,在Redis 4.0之后,引入了LFU算法来解决这个问题

Redis是如何实现LFU的?

LFU 全称是 Least Frequently Used, 最近最不常用的,根据数据访问频率来淘汰数据的,核心思想是如果数据过去被访问多次,那么将来被访问的频率也会更高。

所以,传统LFU算法会记录每个数据的访问次数,当一个数据被再次访问时,递增这个访问次数,这样就能解决前面提到的偶尔被访问了一次后,数据留存很长时间的缓存污染问题。

Redis在实现LFU时,在对象结构中多记录了数据访问频次信息

typedef struct redisObject {
    // 类型,占 4 位
    unsigned type:4;
    // 编码方式,占 4 位
    unsigned encoding:4;
    // LRU 时间(不同模式下含义不同),占 24 位
    unsigned lru:LRU_BITS; 
    // 引用计数
    int refcount;
    // 指向实际数据的指针
    void *ptr;
} robj;

这里头的lru字段,在采用LRU算法和LFU算法的使用方式有所不同

  • 在LRU实现中,该字段24bits全部用来记录key的最后一次访问时间戳
  • 在LFU实现中,该字段分为2段,高16bits(idt)存储key的最后一次访问时间戳,低8bits(logc)记录key的访问频次,值越小表示频率越低,越容易淘汰,每个新加入的key的初始值为5。

image.png 注意,这里的logc并不是单纯的记录访问次数,而是访问频率,会随着时间推移而衰减,在每次key被访问时,先对logc做一个衰减操作,衰减值跟前后访问时间间隔有关,如果上次访问距离这次访问的间隔很长,那么衰减的值就会很大。

也就是说,访问频率需要考虑key的访问是多长时间内发生,如果上次访问距离当前时间越长,那么认为这个key的访问频率也就相应降低了,被淘汰的概率也越大。

对logc做完衰减操作后,就开始对其进行增加操作,不是单纯+1,而是根据概率增加,当前logc值越大的key就越难再增加

综上所述,Redis在访问一个key时,对于logc的变化是:

  1. 先按照上次访问距离这次访问的间隔时长,对logc进行衰减
  2. 再按照一定概率增加logc的值

redis.conf提供了两个配置项,用于调整LFU算法从而控制logc的增长和衰减:

  • lfu-decay-time,调整logc的衰减速度,以分钟为单位,默认值为1,该值越大,衰减越慢
  • lfu-log-factor,调整logc的增长速度,该值越大,增长越慢

参考

《小林coding》