Redis 攻略面经(三)-- 详解内存回收的两种策略

293 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第十五天,点击查看活动详情

📣 大家好,我是Zhan,一名个人练习时长一年半的大二后台练习生🏀

📣 这篇文章是复习 Redis 内存回收的两种策略 的学习笔记📙

📣 如果有不对的地方,欢迎各位指正🙏🏼

📣 与君同舟渡,达岸各自归🌊


👉本篇速览

Redis之所以性能强,最主要就是因为基于内存存储,但是内存是有上限的,如果Redis的内存大小过大,就会影响持久化或者主从同步性能。当内存使用达到上限时,就无法存储更多数据了,为了解决这个问题,Redis提供了两种策略实现内存回收:

  • 过期删除策略:给key设置过期时间,过期的数据将从内存中释放
  • 内存淘汰策略:如果内存已经达到了上限,就会使用内存淘汰策略删除符合条件的key,以保证Redis高效的运行

🛡 过期删除策略

📦 过期时间的存储方式

我们可以给key设置过期时间,那么我们如何判断一个key是否过期呢?这就需要了解过期时间与key是如何在底层存储的:

当我们给一个key设置过期时间的时候,会给 key 带上过期时间存储到一个过期字典中,字典这个可能听起来有点陌生,但实际上字典是哈希表,每个 key 都对应了一个过期时间的时间戳。


Redis 字典的数据结构为:

typedef struct redisDb {
    dict *dict; 
    dict *expires; 
    ....
} redisDb;

我们画张图来表示它的数据结构:

也就是说Redis字典这个数据结构中包含了两个字典

  • 一个是存储数据的key - value字典,用于存储数据的keyvalue的对应关系
  • 另一个是存储过期时间的key - expire字段,用于存储数据的keyexpire的对应关系

那么我们在查询一个key的时候,它的流程为:

  1. 首先会去过期时间的字典中去查找:
    1. 如果过期时间的字典中不存在,代表该数据不会过期,进入下一步
    2. 如果过期时间的字典中存在这个key对应的expire时间戳,那么就去比对存储的时间戳和现在的时间戳:
      1. 如果已经过期,就返回null
      2. 如果没有过期,就进入下一步
  2. 在检验不是过期数据后,就去存储数据的字典中找到对应的数据并返回

🗑 三种过期删除策略

既然数据会过期,那么我们如何去删除这些过期的数据以达到节省内存的目的,Redis给出了三种删除过期的key的策略:定时删除惰性删除定期删除

⏱ 定时删除

定时删除:在设置key的时候,创建一个定时任务,当时间到达时,由事件处理器自动执行删除key的操作。

这样做的优点就是:可以保证这个key过期了马上就会被删除,保证了内存能够最快被释放,对于节省内存作用很大。

但是也会面临问题:如果过期的key比较多,删除过期key会占用CPU的时间也比较多,如果我们的场景是内存够用,但是CPU吃紧,这种策略无疑是雪上加霜,服务器的响应时间会更长,因此对于CPU并不友好


🎯 惰性删除

惰性删除:不会主动的去删除过期的键值对,只会在查询过期的键值对时,发现键值对过期了,然后把它删除(因为它比较懒)

这样做的缺点是:如果一直没有访问过期的键值对,那么内存中会有很多无用的内存,会有一定的内存空间浪费,并不利于节省内存

这么做也有它的优点:因为只会在访问的时候去检验是否过期,它对CPU十分友好,占用的系统资源很少


📆 定期删除

定期删除:定期的去数据库里面随机抽取一定数量的key去检查它是否过期,如果过期就删除,相当于定期给Redis做个体检hhh

这么做的优点就是:由于每次抽取的key不多,那么删除操作执行的时长就不会很长,因此对CPU的影响相较于定时删除小一点

这么做的缺点是:由于是随机抽取key进行判断,因此没有定时删除在内存清理方面效果好,在CPU占用方面又不如惰性删除,并且这个“定期体检”的时长频率不好控制。


💡 Redis 使用的策略

上述三种方策略各有自己的优缺点,如果只是使用一种策略满足不了实际的需求,因此Redis选择了惰性删除 + 定期删除混合使用

刚刚我们也有提到定期删除的频率和时长不好控制,我们来看看Redis是如何实现定期删除的:

  1. 首先就是两次检查间隔的时长,也就是频率,Redis默认每秒进行10次过期检查一次数据库,这个我们可以去Redis的配置文件中去修改
  2. 再就是每次抽取的key的数量,Redis每次会随机选择20个key进行过期的判断

Redis 除了上述的定期删除,还做了一些判断和优化,Redis实现的定期删除的流程如下:

  1. 从过期字典中抽取20个key
  2. 检查这20个key是否过期,如果过期,删除过期的key
  3. 如果过期的数量超过5个,那么会再次重复步骤1,也就是再去抽取20个key,因为Redis想尽可能的保证过期的key的个数占总比不超过25%,如果过期的数量小于5个就等待下一轮检查
  4. Redis为了防止一直循环,或者有线程卡死阻塞的情况,也设置了定期删除的最大时间25ms

🖥 内存淘汰策略

📌 八种内存淘汰策略

当Redis内存达到了上限,我们选择的内存淘汰策略将会决定Redis的做法,一共有一下八种内存淘汰策略:

  1. noeviction:是Redis3.0之后的默认内存淘汰策略,它表示如果达到了上线,Redis将会只读和删除,如果有新的数据插入,就会触发OOM,即不对数据进行删除
  2. volatile-random:随机淘汰设置了过期时间的任意键值
  3. volatile-ttl:优先淘汰更早过期的键值
  4. volatile-lru:是Redis3.0之前的默认内存淘汰策略,会淘汰所有会过期的键值中最久没有使用的键值
  5. volatile-lfu:是Redis4.0后新增的内存淘汰策略,会淘汰所有会过期的键值中最少使用的键值
  6. allkeys-random:会随机淘汰任意的键值
  7. allkeys-lru:淘汰最久没有使用的键值
  8. allkeys-lfu:是Redis4.0后新增的内存淘汰策略,会淘汰整个键值中最少使用的键值

如果我们需要修改内存淘汰策略,我们可以:

  1. 在命令行中进行一次性的修改,也就是说Redis重启后会恢复为默认的内存淘汰策略
config set maxmemory-policy <策略>
  1. 在配置文件中修改,这样Redis重启之后配置不会丢失:
maxmemory-policy <策略>

⚒ LRU 算法

在上面的八种策略中我们可以发现两种策略使用到了lru作为命名的一部分,是因为LRU是一种算法,全称Least Recently Used,也就是最近最少使用

传统的LRU算法基于链表实现,如果链表中的某个元素被操作了,就会移动到链表头部,这样链表从头到尾就代表了他们的使用情况,淘汰时从链表的尾部开始淘汰即可。

但是Redis并没有这么实现,因为在大量数据访问的情况下,移动结点的开销很大,很耗时,并且维护一个链表去管理所有的数据,也会带来很大的空间开销

Redis 实现LRU算法的思路为:给存储的数据加一个属性最后访问时间,这样在进行内存淘汰的时候,采用随机取样,淘汰访问时间最久的那个,这样就不需要维护一个链表并且移动链表中的结点。

LRU算法看似比较好用,但是也存在不合理的地方,比如:

  • A和B两个key,在发生淘汰时的前一个小时前同一时刻添加到Redis
  • A在前49分钟被访问了1000次,但是后11分钟没有被访问
  • B在这一个小时内仅仅第59分钟被访问了1次
  • 此时如果使用LRU算法,如果A、B均被Redis采样选中,A将会被淘汰很显然这个是不合理的

因此在Redis4.0就引入了LFU算法,LFU算法相较于LRU算法更合理。


🛠 LFU 算法

LFU算法全称为Least Frequently Used,也就是最近最不常用,LRU算法是根据使用的时间来淘汰数据,而LFU算法是根据使用的频次来淘汰数据。

在讲解LRU算法的时候,我们提到Redis记录了最后操作时间,使用到的字段是:

typedef struct redisObject {
    ...
    // 24 bits,用于记录对象的访问信息
    unsigned lru:24;  
    ...
} robj;

这个字段在LFU算法和LRU算法中都有使用,但是表示的意义不同:

  1. LRU算法中,它记录了24位的时间戳,也就是我们上面提到的最后一次访问时间
  2. 而在LFU算法中,它的前16位存储访问的时间戳,后8位存储访问的频次

这里要注意,记录的是访问的频次,而非访问的次数

  • 访问的频次在每次访问的时候首先会根据两次访问的时间差做一次减法,两次访问的时间差的越多,频次就减的越多
  • 做完减法之后,会根据概率做加法,并且频次值越大,做加法的概率就越小

最后根据使用的频次去删除数据


💬 总结

本文讲了Redis内存回收的两种策略: 过期删除内存淘汰

  • 对于过期删除,我们分别介绍了:
    • Redis如何存储key和过期时间的对应关系
    • 三种过期删除策略以及他们各自的优缺点
    • Redis使用的过期删除策略
  • 对于内存淘汰,我们分别介绍了:
    • 八种内存淘汰策略,以及修改内存淘汰策略的方法
    • LRU算法 与 Redis实现的类LRU算法
    • 为了弥补LRU算法不足的LFU算法

相信读完今天的文章,大家能对Redis内存的使用有了更深一步的理解~


🍁 友链


✒写在最后

都看到这里啦~,给个点赞再走呗~,也欢迎各位大佬指正以及补充,在评论区一起交流,共同进步!也欢迎加微信一起交流:Goldfish7710。咱们明天见~