【Redis】过期淘汰策略以及内存淘汰机制

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 【Redis】过期淘汰策略以及内存淘汰机制

过期时间的设置

在我们使用Redis的时候,最常使用的就是SET命令了。

SET命令除了可以设置key-value之外,还可以设置key的超时时间,情况如下。

设置完毕超时时间之后可以使用TTL查看对应key的剩余超时时间,单位为秒

而再次对同一个key使用SET命令的时候,并且没有设定超时时间,那么这个key的超时时间就会被擦除,情况如下。

可以发现name的超时时间变为了-1,也就是永不超时的意思。

导致这个问题的原因在于:SET命令如果不设定超时时间,那么Redis会自动擦除这个key的超时时间。

随着对内存占用的不断增长,很多原本设置了超时时间的key,随着后续的使用发现超时时间被擦除了,很可能就是由于这个原因导致的。

这个时候Redis中就会存在着大量的永不超时的key,消耗过多的内存资源。

所以,在使用Redis的SET命令的时候,如果刚开始设定了超时时间,那么之后修改这个key,也务必加上这个超时时间的参数,避免超时时间丢失的问题。

Redis是如何知道一个key是否过期的?

Redis本身是一个典型的key-value内存存储数据库,因此所有的key、value都保存在Dict结构中。不过在其database结构体中,有两个Dict:一个用来记录key-value;另一个用来记录key-TTL。

也就是,Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。

大致如下图

下面是redis源码中的结构体设计

typedef struct redisDb {
  dict *dict; /*存放所有key及value的地方,也被称为keyspace*/
  dict *expires;/*存放每一个key及其对应的TTL存活时间,只包含设置了TTL的key*/
  dict *blocking_keys;/* Keys with clients waiting for data ( BLPOP)*/
  dict *ready_keys;/* Blocked keys that received a PUSH */
  dict *watched_keys;/* WATCHED keys for MULTI/EXEC CAS */
  int id;/* Database ID,0~15 */
  long long avg ttl;  /*记录平均TTL时长*/
  unsigned long expires_cursor;/* expire检查时在dict中抽样的索弓位置.*/
  list *defrag_later;/*等待碎片整理的key列表.*/
}redisDb;

下面是Dict结构中每一个存放键值数据的节点。

其中key用于存放Redis中插入进去的键,v存放redis中插入进去的值或者超时时间。

typedef struct dictEntry {
  void *key; l/键
  union {
    void *val;
    uint6_t u64;
    int64_t s64;
    double d;} v;//值
  //下一个Entry的指针
  struct dictEntry *next;
}dictEntry ;

而Redis只需要通过这两个dict就可以得知对应的key对应的超时时间了。

Redis的两种过期key删除策略

  • 惰性删除:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。当读/写一个key的时候,会判断key是否超时,如果是,那么就立即删除掉这个key,如果没有访问到,那么这个key有可能一直存在于内存中。
  • 周期删除/定时删除:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。由于惰性删除策略无法保证冷数据被及时的删除,所以Redis会定期(默认每100ms)主动淘汰一批已经过期的key,这里的一批只是部分过期的key,所以可能会出现部分key已经过期但是还没有被删除的情况,导致占用的内存没有被释放。

定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。

这里重点讲解周期删除

周期删除顾明思议是通过一个定时任务,周期性的抽样部分过期的key,然后执行删除。执行周期有两种:

  • Redis会设置一个定时任务serverCron(),按照server.hz的频率来执行过期key清理,模式为SLOW
  • Redis的每个事件循环前会调用beforeSleep()函数,执行过期key清理,模式为FAST

SLOW模式规则

① 执行频率受server.hz影响,默认为10,即每秒执行10次,每个执行周期100ms

② 执行清理耗时不超过一次执行周期的25%(25ms)

③ 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期

④ 如果没达到时间上限(25ms)并且过期key比例大于10%,再进行一次抽样,否则结束。

FAST模式规则(过期key比例小于10%不执行)

① 执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms

② 执行清理耗时不超过1ms

③ 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期

④ 如果没达到时间上限(1ms)并且过期key比例大于10%,再进行一次抽样,否则结束

上面说了这么多,还是有可能有很多的过期key存在于内存中,那么怎么办?

答案就是----Redis的内存淘汰机制

Redis内存淘汰机制

Redis在执行每一个命令之前都需要检测当前使用的内存空间,如果当前使用的内存已经超过了设定的maxmemory,那么Redis就会拒绝这次命令,并且主动触发内存淘汰策略,内存淘汰策略会主动删除一些key,删除这些key将会按照一定的规则。

内存淘汰就是当Redis内存使用达到设置的阈值时,Redis主动挑选部分key删除以释放更多内存的流程。Redis会在处理客户端命令的方法processCommand()中尝试做内存淘汰:

int processCommand(client *c) {
//如果服务器设置了server.maxmemory属性,并且并未有执行lua脚本
  if (server.maxmemory && !server.lua_timedout) {
    //尝试进行内存淘汰performEvictions
    int out_of_memory = (performEvictions() == EVICT_FAIL);
    // .......
    if (out_of_memory && reject_cmd_on_oom){ //拒绝请求
      rejectCommand(c, shared. oomerr);
      return C_OK;
    }
  }
}

Redis总共支持八种不同策略来选择要删除的key。

  • noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
  • volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
  • allkeys-random:对全体key,随机进行淘汰。也就是直接从db->dict中随机挑选
  • volatile-random:对设置了TTL的key,随机进行淘汰。也就是从db->expires中随机挑选。
  • allkeys-lru:对全体key,基于LRU算法进行淘汰
  • volatile-lru:对设置了TTL的key,基于LRU算法进行淘汰
  • allkeys-lfu: 对全体key,基于LFU算法进行淘汰
  • volatile-lfu:对设置了TTL的key,基于LFI算法进行淘汰

比较容易混淆的有两个:

LRU (Least Recently Used),最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

LFU (Least Frequently Used),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。

而如果使用LFU和LRU相关的算法,那么Redis就需要知道最近使用的频率和最近使用的时间,那么Redis是如何知道这些数据的呢?

Redis的数据都会被封装为RedisObject结构:

typedef struct redisobject {
    unsigned type:4;    //对象类型
    unsigned encoding: 4; //编码方式
    unsigned lru:LRU_BITS;  // LRU:以秒为单位记录最近一次访问时间,长度24bit
                        // LFU:高16位以分钟为单位记录最近一次访问时间,低8位记录逻辑访问次数
    int refcount;   //引用计数,计数为0则可以回收
    void *ptr;      //数据指针,指向真实数据
}robj;

LFU的访问次数之所以叫做逻辑访问次数,是因为并不是每次key被访问都计数,而是通过运算:

① 生成0~1之间的随机数R

② 计算1/(旧次数 * lfu_log_factor + 1),记录为P,lfu_log_factor默认为10

③ 如果R<P,则计数器+1,且最大不超过255

④ 访问次数会随时间衰减,距离上一次访问时间每隔lfu_decay_time分钟(默认1),计数器-1

现在用下面这张图介绍Redis的内存淘汰机制。

首先命令过来的时候先判断内存是否足够,如果是直接继续。如果不够,那么就判断是否开启了内存淘汰策略,如果没有开启,那么直接报错返回内存不够。如果开启了,那么进行判断,判断是对所有的key进行淘汰,还是只对设置了超时时间的key进行淘汰。

如果是对所有的key进行淘汰,那么直接从db中的dict进行操作,否则从db中的entries进行操作。

然后继续判断内存淘汰机制,如果是随机淘汰,那么直接随机淘汰一些key,然后判断是否内存够用,如果还是不够就继续循环,够了就结束。

如果不是选择随机淘汰,那么就创建一个淘汰池,然后获取数据库,从这些数据库中选择一些样本数据,之所以选择原本数据是因为如果对全部数据进行遍历比较进行LRU或者LFU算法,那么效率很低,因此直接抽一些原本也是够用的。

样本选择完毕之后,根据淘汰策略进行选择。

由于淘汰池设定的是升序淘汰,也就是存入淘汰池中的idleTime越大,那么越先淘汰。

同时如果淘汰池已经满了,那么再次插入数据的时候就需要进行判断了,如果要插入的idleTime更大,那么就把最小的放回去,把这次的插进去。(也就是比idleTime更大,越大越先淘汰)

因此如果是TTL淘汰,那么就用maxTTL(常量)-TTL(当前超时时间),如果这个差值很大,说明TTL很小,本身就快超时了,对所有的原本数据进行这个操作之后,选出部分差值最大的key将他们放入淘汰池。也就是按照idleTime升序插入到淘汰池中。也就是idleTime越大的在越下面,所以进行淘汰的时候倒叙遍历倒序淘汰数据即可。

然后继续判断下一个数据库,因此如果只是用一个数据库,那么效率就更好了。

如果没有下一个数据库就从淘汰池底部(以为是升序,越后面的数据越先淘汰)抽取一个key进行淘汰,然后进行判断内存是否足够,不足够继续删除,足够了停止删除。

相信讲解到这里你就明白为什么有些key没有设定超时时间却也被删除了。


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
10天前
|
缓存 监控 NoSQL
阿里面试让聊一聊Redis 的内存淘汰(驱逐)策略
大家好,我是 V 哥。粉丝小 A 面试阿里时被问到 Redis 的内存淘汰策略问题,特此整理了一份详细笔记供参考。Redis 的内存淘汰策略决定了在内存达到上限时如何移除数据。希望这份笔记对你有所帮助!欢迎关注“威哥爱编程”,一起学习与成长。
|
11天前
|
存储 Prometheus NoSQL
Redis 内存突增时,如何定量分析其内存使用情况
【9月更文挑战第21天】当Redis内存突增时,可采用多种方法分析内存使用情况:1)使用`INFO memory`命令查看详细内存信息;2)借助`redis-cli --bigkeys`和RMA工具定位大键;3)利用Prometheus和Grafana监控内存变化;4)优化数据类型和存储结构;5)检查并调整内存碎片率。通过这些方法,可有效定位并解决内存问题,保障Redis稳定运行。
|
3天前
|
缓存 NoSQL 算法
14)Redis 在内存用完时会怎么办?如何处理已过期的数据?
14)Redis 在内存用完时会怎么办?如何处理已过期的数据?
9 0
|
3天前
|
存储 缓存 NoSQL
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
10 0
|
3天前
|
存储 NoSQL Redis
Redis的RDB快照:保障数据持久性的关键机制
Redis的RDB快照:保障数据持久性的关键机制
10 0
|
3天前
|
存储 缓存 NoSQL
深入探究Redis的AOF持久化:保障数据安全与恢复性能的关键机制
深入探究Redis的AOF持久化:保障数据安全与恢复性能的关键机制
13 0
|
14天前
|
NoSQL Java API
Redis数据淘汰策略的详细介绍
通过上述步骤,我们不仅解决了一个实际问题,也进一步了解了Java 8时间API的强大功能和灵活性。希望这个解答能够帮助你在日常开发中更加自如地处理时间和时区相关的问题。
23 0
|
19天前
|
监控 算法 数据可视化
深入解析Android应用开发中的高效内存管理策略在移动应用开发领域,Android平台因其开放性和灵活性备受开发者青睐。然而,随之而来的是内存管理的复杂性,这对开发者提出了更高的要求。高效的内存管理不仅能够提升应用的性能,还能有效避免因内存泄漏导致的应用崩溃。本文将探讨Android应用开发中的内存管理问题,并提供一系列实用的优化策略,帮助开发者打造更稳定、更高效的应用。
在Android开发中,内存管理是一个绕不开的话题。良好的内存管理机制不仅可以提高应用的运行效率,还能有效预防内存泄漏和过度消耗,从而延长电池寿命并提升用户体验。本文从Android内存管理的基本原理出发,详细讨论了几种常见的内存管理技巧,包括内存泄漏的检测与修复、内存分配与回收的优化方法,以及如何通过合理的编程习惯减少内存开销。通过对这些内容的阐述,旨在为Android开发者提供一套系统化的内存优化指南,助力开发出更加流畅稳定的应用。
38 0
|
2月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
|
3月前
|
存储 分布式计算 Hadoop
HadoopCPU、内存、存储限制
【7月更文挑战第13天】
213 14
下一篇
无影云桌面