文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Redis内存满了怎么办?让你玩懂8种内存淘汰策略

2024-12-03 09:49

关注

本文转载自微信公众号「moon聊技术」,作者moon聊技术。转载本文请联系moon聊技术公众号。

简介

我们知道redis是一个非常常用的内存型数据库,数据从内存中读取是它非常高效的原因之一,那么但是如果有一天,「redis分配的内存满了怎么办」?遇到这个面试题不要慌,这种问题我们分为两角度回答就可以:

增加redis可用内存

这种方法很暴力,也很好用,我们直接通过增加redis的可用内存就可以了, 有两种方式

「通过配置文件配置」

  1. //设置redis最大占用内存大小为1000M   
  2. maxmemory 1000mb  

通过在redis安装目录下面的redis.conf配置文件中添加以下配置设置内存大小

「通过命令修改」

  1. //设置redis最大占用内存大小为1000M   
  2. 127.0.0.1:6379> config set maxmemory 1000mb   

这种方法是立竿见影的,reids 内存总归受限于机器的内存,也不能无限制的增长,那么如果没有办法再增加 redis 的可用内存怎么办呢?

内存淘汰策略

实际上Redis定义了「8种内存淘汰策略」用来处理redis内存满的情况:

noeviction:直接返回错误,不淘汰任何已经存在的redis键

allkeys-lru:所有的键使用lru算法进行淘汰

volatile-lru:有过期时间的使用lru算法进行淘汰

allkeys-random:随机删除redis键

volatile-random:随机删除有过期时间的redis键

volatile-ttl:删除快过期的redis键

volatile-lfu:根据lfu算法从有过期时间的键删除

allkeys-lfu:根据lfu算法从所有键删除

这些内存淘汰策略都很好理解,我们着重讲解一下lru,lfu,ttl是怎么去实现的

lru的最佳实践?

lru是Least Recently Used的缩写,也就是「最近很少使用」,也可以理解成最久没有使用。最近刚刚使用过的,后面接着会用到的概率也就越大。由于内存是非常金贵的,导致我们可以存储在缓存当中的数据是有限的。比如说我们固定只能存储1w条,当内存满了之后,缓存每插入一条新数据,都要抛弃一条最长没有使用的旧数据。我们把上面的内容整理一下,可以得到几点要求:

所以我们要尽可能的保证查询效率很高,插入效率很高,我们知道如果只考虑查询效率,那么hash表可能就是最优的选择,如果只考虑插入效率,那么链表必定有它的一席之地。

但是这两种数据结构单独使用,都有它的弊端,那么说,有没有一种数据结构,既能够保证查询效率,又能够保证插入效率呢?于是 hash+链表这种结构出现了

hash表用来查询在链表中的数据位置,链表负责数据的插入 当新数据插入到链表头部时有两种情况;

这时双向链表的作用也提现出来了。能直接定位到父节点。这效率就很高了。而且由于双向链表有尾指针,所以剔除最后的尾节点也十分方便,快捷

所以最终的解决方案就是采用「哈希表+双向链表」的结构

lfu的最佳实践?

LFU:Least Frequently Used,最不经常使用策略,在一段时间内,数据被「使用频次最少」的,优先被淘汰。最少使用(LFU)是一种用于管理计算机内存的缓存算法。主要是记录和追踪内存块的使用次数,当缓存已满并且需要更多空间时,系统将以最低内存块使用频率清除内存.采用LFU算法的最简单方法是为每个加载到缓存的块分配一个计数器。每次引用该块时,计数器将增加一。当缓存达到容量并有一个新的内存块等待插入时,系统将搜索计数器最低的块并将其从缓存中删除。

这里我们提出一种达到 O(1) 时间复杂度的 LFU 实现方案,它支持的操作包括插入、访问以及删除

如图:

由两个双向链表+哈希表组成,上方的双向链表用来计数,下方的双向链表用来记录存储的数据,该链表的头节点存储了数字,哈希表的value对象记录下方双向链表的数据 我们这里按照插入的流程走一遍:

这样当查找,插入时效率都为O(1)

redis TTL 是怎么实现的?

TTL存储的数据结构

redis针对TTL时间有专门的dict进行存储,就是redisDb当中的dict *expires字段,dict顾名思义就是一个hashtable,key为对应的rediskey,value为对应的TTL时间。?dict的数据结构中含有2个dictht对象,主要是为了解决hash冲突过程中重新hash数据使用。

TTL 设置过期时间

TTL设置key过期时间的方法主要是下面4个:

  1. {"expire",expireCommand,3,"w",0,NULL,1,1,1,0,0}, 
  2. {"expireat",expireatCommand,3,"w",0,NULL,1,1,1,0,0}, 
  3. {"pexpire",pexpireCommand,3,"w",0,NULL,1,1,1,0,0}, 
  4. {"pexpireat",pexpireatCommand,3,"w",0,NULL,1,1,1,0,0}, 

expire expireat pexpire pexpireat

从实际设置过期时间的实现函数来看,相对时间的策略会有一个当前时间作为基准时间,绝对时间的策略会「以0作为一个基准时间」。

  1. void expireCommand(redisClient *c) { 
  2.     expireGenericCommand(c,mstime(),UNIT_SECONDS); 
  3.  
  4. void expireatCommand(redisClient *c) { 
  5.     expireGenericCommand(c,0,UNIT_SECONDS); 
  6.  
  7. void pexpireCommand(redisClient *c) { 
  8.     expireGenericCommand(c,mstime(),UNIT_MILLISECONDS); 
  9.  
  10. void pexpireatCommand(redisClient *c) { 
  11.     expireGenericCommand(c,0,UNIT_MILLISECONDS); 

整个过期时间最后都会换算到绝对时间进行存储,通过公式基准时间+过期时间来进行计算。?对于相对时间而言基准时间就是当前时间,对于绝对时间而言相对时间就是0。?中途考虑设置的过期时间是否已经过期,如果已经过期那么在master就会删除该数据并同步删除动作到slave。?正常的设置过期时间是通过setExpire方法保存到 dict *expires对象当中。

  1.  
  2. void expireGenericCommand(redisClient *c, long long basetime, int unit) { 
  3.    robj *key = c->argv[1], *param = c->argv[2]; 
  4.    long long when;  
  5.  
  6.    // 取出 when 参数 
  7.    if (getLongLongFromObjectOrReply(c, param, &whenNULL) != REDIS_OK) 
  8.        return
  9.  
  10.    // 如果传入的过期时间是以秒为单位的,那么将它转换为毫秒 
  11.    if (unit == UNIT_SECONDS) when *= 1000; 
  12.    when += basetime; 
  13.  
  14.     
  15.    // 取出键 
  16.    if (lookupKeyRead(c->db,key) == NULL) { 
  17.        addReply(c,shared.czero); 
  18.        return
  19.    } 
  20.  
  21.     
  22.    if (when <= mstime() && !server.loading && !server.masterhost) { 
  23.  
  24.        // when 提供的时间已经过期,服务器为主节点,并且没在载入数据 
  25.  
  26.        robj *aux; 
  27.  
  28.        redisAssertWithInfo(c,key,dbDelete(c->db,key)); 
  29.        server.dirty++; 
  30.  
  31.         
  32.        // 传播 DEL 命令 
  33.        aux = createStringObject("DEL",3); 
  34.  
  35.        rewriteClientCommandVector(c,2,aux,key); 
  36.        decrRefCount(aux); 
  37.  
  38.        signalModifiedKey(c->db,key); 
  39.        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id); 
  40.  
  41.        addReply(c, shared.cone); 
  42.  
  43.        return
  44.    } else { 
  45.  
  46.        // 设置键的过期时间 
  47.        // 如果服务器为附属节点,或者服务器正在载入, 
  48.        // 那么这个 when 有可能已经过期的 
  49.        setExpire(c->db,key,when); 
  50.  
  51.        addReply(c,shared.cone); 
  52.  
  53.        signalModifiedKey(c->db,key); 
  54.        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id); 
  55.  
  56.        server.dirty++; 
  57.  
  58.        return
  59.    } 
  60.  
  61.  setExpire函数主要是对db->expires中的key对应的dictEntry设置过期时间。 
  62.  
  63.  
  64. void setExpire(redisDb *db, robj *key, long long when) { 
  65.  
  66.    dictEntry *kde, *de; 
  67.  
  68.     
  69.    // 取出键 
  70.    kde = dictFind(db->dict,key->ptr); 
  71.  
  72.    redisAssertWithInfo(NULL,key,kde != NULL); 
  73.  
  74.    // 根据键取出键的过期时间 
  75.    de = dictReplaceRaw(db->expires,dictGetKey(kde)); 
  76.  
  77.    // 设置键的过期时间 
  78.    // 这里是直接使用整数值来保存过期时间,不是用 INT 编码的 String 对象 
  79.    dictSetSignedIntegerVal(de,when); 

redis什么时候执行淘汰策略?

在redis种有三种删除的操作此策略

巨人的肩膀

https://www.jianshu.com/p/53083f5f2ddc https://zhuanlan.zhihu.com/p/265597517

 

来源:moon聊技术内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯