【Redis】内存淘汰策略和过期删除策略

news2024/11/24 4:52:57

图片

一、前言

Redis是一个内存键值对数据库,所以对于内存的管理尤为重要。Redis内部对于内存的管理主要包含两个方向,过期删除策略和数据淘汰策略。由于内存有限,缓存系统必须遵循一些淘汰策略来删除一些不再需要的键,以便为新键腾出空间。在Redis中,缓存淘汰策略是由maxmemory和maxmemory-policy两个配置参数控制的。

二、淘汰策略原理

所谓数据淘汰是指在Redis内存使用达到一定阈值的时候,执行某种策略释放内存空间,以便于接收新的数据。内存可使用空间由配置参数maxmemory决定(单位mb/GB)。故又叫"最大内存删除策略",也叫"缓存删除策略"。

1. 内存上限配置

Redis 的最大内存上限可以在配置文件 redis.conf 中配置,redis.conf 配置如下:

# maxmemory <bytes>

设置 maxmemory 为 0 表示没有内存限制。在 64 位系统中,默认是 0 无限制,但是在 32 位系统中默认是 3GB。

2. 淘汰策略配置

淘汰策略配置maxmemory-policy,表示当内存达到maxmemory时,将执行配置的淘汰策略,由redis.c/freeMemoryIfNeeded 函数实现数据淘汰逻辑。

3. maxmemory-policy配置

# maxmemory-policy noeviction

4. freeMemoryIfNeeded逻辑处理


int freeMemoryIfNeeded(void) {
  size_t mem_used, mem_tofree, mem_freed;
  int slaves = listLength(server.slaves);

  /* Remove the size of slaves output buffers and AOF buffer from the count of used memory.*/
  // 计算出 Redis 目前占用的内存总数,但有两个方面的内存不会计算在内:
  // 1)从服务器的输出缓冲区的内存
  // 2)AOF 缓冲区的内存
  mem_used = zmalloc_used_memory();
  if (slaves) {
    listIter li;
    listNode *ln;

    listRewind(server.slaves,&li);
    while((ln = listNext(&li))) {
      redisClient *slave = listNodeValue(ln);
      unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);
      if (obuf_bytes > mem_used)
        mem_used = 0;
      else
        mem_used -= obuf_bytes;
    }
  }
  if (server.aof_state != REDIS_AOF_OFF) {
    mem_used -= sdslen(server.aof_buf);
    mem_used -= aofRewriteBufferSize();
  }

  /* Check if we are over the memory limit. */
  // 如果目前使用的内存大小比设置的 maxmemory 要小,那么无须执行进一步操作
  if (mem_used <= server.maxmemory) return REDIS_OK;

  // 如果占用内存比 maxmemory 要大,但是 maxmemory 策略为不淘汰,那么直接返回
  if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
    return REDIS_ERR; /* We need to free memory, but policy forbids. */

  /* Compute how much memory we need to free. */
  // 计算需要释放多少字节的内存
  mem_tofree = mem_used - server.maxmemory;

  // 初始化已释放内存的字节数为 0
  mem_freed = 0;

  // 根据 maxmemory 策略,
  // 遍历字典,释放内存并记录被释放内存的字节数
  while (mem_freed < mem_tofree) {
    int j, k, keys_freed = 0;

    // 遍历所有字典
    for (j = 0; j < server.dbnum; j++) {
      long bestval = 0; /* just to prevent warning */
      sds bestkey = NULL;
      dictEntry *de;
      redisDb *db = server.db+j;
      dict *dict;

      if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
        server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
      {
        // 如果策略是 allkeys-lru 或者 allkeys-random 
        // 那么淘汰的目标为所有数据库键
        dict = server.db[j].dict;
      } else {
        // 如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl 
        // 那么淘汰的目标为带过期时间的数据库键
        dict = server.db[j].expires;
      }

      // 跳过空字典
      if (dictSize(dict) == 0) continue;

      /* volatile-random and allkeys-random policy */
      // 如果使用的是随机策略,那么从目标字典中随机选出键
      if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
        server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
      {
        de = dictGetRandomKey(dict);
        bestkey = dictGetKey(de);
      }

      /* volatile-lru and allkeys-lru policy */
      // 如果使用的是 LRU 策略,
      // 那么从一集 sample 键中选出 IDLE 时间最长的那个键
      else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
        server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
      {
        struct evictionPoolEntry *pool = db->eviction_pool;

        while(bestkey == NULL) {
          evictionPoolPopulate(dict, db->dict, db->eviction_pool);
          /* Go backward from best to worst element to evict. */
          for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k--) {
            if (pool[k].key == NULL) continue;
            de = dictFind(dict,pool[k].key);

            /* Remove the entry from the pool. */
            sdsfree(pool[k].key);
            /* Shift all elements on its right to left. */
            memmove(pool+k,pool+k+1,
              sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1));
            /* Clear the element on the right which is empty since we shifted one position to the left.  */
            pool[REDIS_EVICTION_POOL_SIZE-1].key = NULL;
            pool[REDIS_EVICTION_POOL_SIZE-1].idle = 0;

            /* If the key exists, is our pick. Otherwise it is a ghost and we need to try the next element. */
            if (de) {
              bestkey = dictGetKey(de);
              break;
            } else {
              /* Ghost... */
              continue;
            }
          }
        }
      }

      /* volatile-ttl */
      // 策略为 volatile-ttl ,从一集 sample 键中选出过期时间距离当前时间最接近的键
      else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
        for (k = 0; k < server.maxmemory_samples; k++) {
          sds thiskey;
          long thisval;

          de = dictGetRandomKey(dict);
          thiskey = dictGetKey(de);
          thisval = (long) dictGetVal(de);

          /* Expire sooner (minor expire unix timestamp) is better candidate for deletion */
          if (bestkey == NULL || thisval < bestval) {
            bestkey = thiskey;
            bestval = thisval;
          }
        }
      }

      /* Finally remove the selected key. */
      // 删除被选中的键
      if (bestkey) {
        long long delta;

        robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
        propagateExpire(db,keyobj);
        // 计算删除键所释放的内存数量
        delta = (long long) zmalloc_used_memory();
        dbDelete(db,keyobj);
        delta -= (long long) zmalloc_used_memory();
        mem_freed += delta;
        
        // 对淘汰键的计数器增一
        server.stat_evictedkeys++;

        notifyKeyspaceEvent(REDIS_NOTIFY_EVICTED, "evicted",
            keyobj, db->id);
        decrRefCount(keyobj);
        keys_freed++;

        /* When the memory to free starts to be big enough, we may */
        /* start spending so much time here that is impossible to */
        /* deliver data to the slaves fast enough, so we force the */
        /* transmission here inside the loop. */
        if (slaves) flushSlavesOutputBuffers();
      }
    }

    if (!keys_freed) return REDIS_ERR; /* nothing to free... */
  }

  return REDIS_OK;
}

策略例子:

maxmemory 10mb
maxmemory-policy volatile-lru

在此示例中,当Redis的内存使用达到10MB时,Redis将从过期的键中使用LRU算法进行淘汰。

三、淘汰策略分类

Redis 提供了一种内存淘汰策略,当内存不足时,Redis 会根据相应的淘汰规则对 key 数据进行淘汰。Redis 的内存淘汰策略共有 8 种具体的淘汰策略,默认的策略为 noeviction,当内存使用达到阈值的时候, 所有引起申请内存的命令会报错。8 种淘汰策略如下所示。

  1. noeviction:当内存不足时,禁止淘汰数据,写入操作报错。这是 Redis 默认的内存淘汰策略。

  2. volatile-lru:当内存不足时,从设置了过期时间的 key 中使用 LRU 算法,选出最近使用最少的数据进行淘汰;

  3. allkeys-lru:当内存不足时,从所有 key 中使用 LRU 算法,选出最近使用最少的数据进行淘汰;

  4. volatile-lfu:当内存不足时,从设置了过期时间的 key 中使用 LFU 算法,选出使用频率最低的数据进行淘汰;

  5. allkeys-lfu:当内存不足时,从所有 key 中使用 LFU 算法,选出使用频率最低的数据,进行淘汰;

  6. volatile-random:当内存不足时,从设置了过期时间的 key 中,随机选出数据进行淘汰;

  7. allkeys-random:当内存不足时,从所有的 key 中,随机选出数据进行淘汰;

  8. volatile-ttl:当内存不足时,从设置了过期时间的 key 中,选出即将过期的数据(按照过期时间的先后,选出最先过期的数据)进行淘汰;

前缀为 volatile- 和 allkeys- 的区别在于二者选择要清除的键时的字典不同,volatile- 前缀的策略表示从 redisDb 中的过期字典中选择键进行清除;allkeys- 开头的策略代表从 dict 字典中选择键进行清除。

策略中用到的两种算法:

LRU(Least Recently Used):最近最少使用。优先淘汰使用时间最远的数据。

LFU(Least Frequently Used):最小频率使用。优先淘汰最不常用的 Key。

淘汰策略的选择

存在冷热数据区别,即意味着访问频率存在较大差异,4.0及以上版本建议选择allkeys-lfu策略,但要设置lfu-decay-time 计数衰减值,一般默认1,这样可避免缓存污染现象;3.0及以下版本建议选择allkeys-lru策略。

LFU访问计数衰减配置​​​​​​​

# The counter decay time is the time, in minutes, that must elapse in order
# for the key counter to be divided by two (or decremented if it has a value
# less <= 10).
#
# The default value for the lfu-decay-time is 1. A special value of 0 means to
# decay the counter every time it happens to be scanned.
#
lfu-decay-time 1

内存淘汰的工作步骤

  • 客户端执行一条新命令,导致数据库需要增加数据(比如 set key value);

  • Redis 会检查内存使用情况,如果内存使用超过 maxmemory 设置的值,就会按照内存淘汰策略删除一些 key;

  • 新的命令执行成功。

四、过期删除策略

众所周知,在Redis的实际使用过程中,为了让可贵的内存得到更高效的利用,我们提倡给每一个key配置合理的过期时间,以避免因内存不足,或因数据量过大而引发的请求响应延迟甚至是不可用等问题。

过期时间底层原理:当key设置了过期时间,Redis内部会将这个key带上过期时间放入过期字典(expires)中,当进行查询时,会先在过期字典中查询是否存在该键,若存在则与当前UNIX时间戳做对比来进行过期时间判定。

常见删除方式

定时删除:在写入key之后,根据否配置过期时间生成特定的定时器,定时器的执行时间就是具体的过期时间。用CPU性能换去内存存储空间——即用时间获取空间

定期删除:提供一个固定频率的定时器,执行时扫描所有的key进行过期检查,满足条件的就进行删除。

惰性删除:数据不做及时释放,待下一次接收到读写请求时,先进行过期检查,若已过期则直接删除。用内存存储空间换取CPU性能——即用空间换取时间。

图片

Redis过期删除策略

由上述三种常用的删除方式对比结果可知,单独的使用任何一种方式都不能达到比较理想的结果,因此Redis的作者在设计过期删除策略的时候,结合了定期删除与惰性删除两种方式来完成。

定期删除:内部通过redis.c/activeExpireCycle函数,以一定的频率运行,每次运行从数据库中随机抽取一定数量的key进行过期检查,若检查通过,则对该数据进行删除。在2.6版本中,默认每秒10次,在2.8版本后可通过redis.config配置文件的hz属性对频率进行设置,,官方建议数值不要超过100,否则将对CPU性能有重大影响。

# The range is between 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is required.
hz 10

惰性删除:内部通过redis.c/expireIfNeeded函数,在每次执行读写操作指令之前,进行过期检查。若已设置过期时间且已过期,则删除该数据。

图片

由表格可知,这两种方式的结合,能很好的解决过期数据滞留内存的问题,同时也很好的保证了数据的一致性,保证了内存使用的高效与CPU的性能。

过期删除策略引起的脏读现象

  • 在单节点实例模式下,因为Redis是单线程模型,所以过期策略可以保证数据一致性。

  • 在集群模式下,过期删除策略会引起脏读现象:

    • 数据的删除在主库执行,从库不会执行。对于惰性删除策略来说,3.2版本以前,从库读取数据时哪怕数据已过期还是会返回数据,3.2版本以后,则会返回空。

    • 对于定期删除策略,由于只是随机抽取了一定的数据,此时已过期但未被命中删除的数据在从库中读取会出现脏读现象。

    • 过期时间命令EX|PX,在主从同步时,因为同步需要时间,就会导致主从库实际过期时间出现偏差。比如主库设置过期时间60s,但同步全量花费了1分钟,那么在从库接收到命令并执行之后,就导致从库key的过期时间整体跨越了两分钟,而此时主库在一分钟之前数据就已经过期了。EXAT|PXAT 命令来设置过期时间节点。这样可避免增量同步的发生。但需注意主从服务器时间一致。

在实际使用过程中,过期时间配置只是一种常规手段,当key的数量在短时间内突增,就有可能导致内存不够用。此时就需要依赖于Redis内部提供的淘汰策略来进一步的保证服务的可用性。

图片

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1405999.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【学习】FPN特征金字塔

论文&#xff1a;Feature Pyramid Networks for Object Detection &#xff08;CVPR 2016) 参考blog&#xff1a;https://blog.csdn.net/weixin_55073640/article/details/122627966 参考视频讲解&#xff1a;添加链接描述 卷积网络中&#xff0c;深层网络容易响应语义特征&am…

java对代码进行性能分析

使用Instant对象来完成 关键代码: 获取当前时间 Instant now Instant.now(); 获取当前时间距离1970-01-01 00:00:00的秒数 now.getEpochSecond() 完整测试代码 Scanner sc new Scanner(System.in);Instant now1 Instant.now();System.out.println("请随便输个东西:…

进程线程知识

一 初识linux线程 1 线程由来 我们之前说创建一个进程&#xff0c;要创建进程控制块pcb&#xff0c;进程地址空间&#xff0c;页表&#xff0c;而且我之前的博客中都有意无意的说明这个pcb是描述进程的&#xff0c;是os用来管理进程的&#xff0c;而有了线程后&#xff0c;就要…

软件测试|SQL常用语法,你都会吗?

前言 SQL作为一门语言&#xff0c;和其他编程语言一样&#xff0c;都是需要遵循一些特定的规范和准则的&#xff0c;这也就是我们常说的语法&#xff08;Syntax&#xff09;。 下面是几个SQL的语法规则&#xff1a; 所有的 SQL 语法都必须以关键字&#xff08;也称命令&…

品牌如何把“流量”转为“留量”,媒介盒子分享

如果品牌一味追逐流量只能抓住一时的红利期&#xff0c;因此品牌需要把“流量”转为“留量”。依靠流量红利快速崛起的红利品牌&#xff0c;在流量法则失效后&#xff0c;就会陷入增长困境&#xff0c;今天媒介盒子就来和大家聊聊品牌如何把“流量”转为“留量”。 一、 差异化…

力扣279. 完全平方数

动态规划 思路&#xff1a; 假设 dp[i] 为最少组成数 i 的平方数个数&#xff1b;则其上一个状态为 dp[i - j^2] 1&#xff0c;1 为 j^2&#xff1a; 即 i 的最少完全平方数 i - j^2 的最少完全平方数 1&#xff0c;其中 j^2 < i 为最接近 i 的平方数&#xff1b;初始值…

pikachu_ssrf攻略

ssrf&#xff08;curl&#xff09;&#xff1a; 打开pikachu靶场&#xff1a; http://127.0.0.1/pikachu-master/vul/ssrf/ssrf_curl.php?urlhttp://127.0.0.1/pikachu-master/vul/ssrf/ssrf_info/info1.php 发现URL地址最后面是info1.php 猜测一下有没有可能存在info2.php?…

【Linux工具篇】编辑器vim

目录 vim的基本操作 进入vim(正常模式&#xff09; 正常模式->插入模式 插入模式->正常模式 正常模式->底行模式 底行模式->正常模式 底行模式->退出vim vim正常模式命令集 vim插入模式命令集 vim末行模式命令集 vim操作总结 vim配置 Linux编译器…

Original PIPE and Serdes PIPE

PIPE is PHY Interface for PCIE ,STAT, USB, DispalyPort and Converged IO&#xff0c;上述协议都可以使用这种通用接口。 它是连接物理层PHY 与 MAC( 或者是Link layer ASIC)。PIPE 的技术规范是在上述几种规范基础上发展来的&#xff0c;如果PIPE规范与上述规范有冲突以上述…

无刷电机学习-方波电调 电路篇

想要彻底的理解无刷电机的驱动&#xff0c;那必然少不了学习他的驱动电路和程序。这里用开源的AM32无刷电调&#xff08;方波驱动&#xff09;来作为学习无刷电机笔记。 https://github.com/AlkaMotors附上作者github地址 AM32_Hardware: 基于AT32MCU的AM32PCB另一位大佬开源…

31、WEB攻防——通用漏洞文件上传JS验证mimeuser.ini语言特性

文章目录 文件上传一、前端验证二、.user.ini 文件上传 检测层面&#xff1a;前端、后端等检测内容&#xff1a;文件头、完整性、二次渲染等检测后缀&#xff1a;黑名单、白名单、MIME检测等绕过技巧&#xff1a;多后缀解析&#xff08;php5、php7&#xff09;、截断、中间件特…

golang通过go-git下载gitlab源码

1 申请令牌 方法1&#xff1a;具体项目下申请&#xff1a; 方法2&#xff1a;全局申请 2 获取token 3 下载代码 package mainimport ("fmt""os""github.com/go-git/go-git/v5" )func main() {_, err : git.PlainClone("/tmp/foo",…

力扣hot100 乘积最大子数组 DP

Problem: 152. 乘积最大子数组 文章目录 思路复杂度&#x1f496; Code 思路 &#x1f468;‍&#x1f3eb; 参考题解 复杂度 时间复杂度: O ( n ) O(n) O(n) 空间复杂度: O ( n ) O(n) O(n) &#x1f496; Code public class Solution {public int maxProduct(int[]…

ruoyi框架教程

心血来潮&#xff0c;写一篇关于ruoyi【若依】框架从0-1的教程。说一下使用感受吧&#xff0c;如果有一个架构师或者老手已经把架构改造完成也能指导你如何快速上手&#xff0c;那么你在后面的增删改查系列开发起来会如鱼得水。如果没有人改造&#xff0c;也没有人教你&#xf…

【江科大】STM32:中断系统(理论)

文章目录 中断系统为什么要使用中断中断优先级中断嵌套STM32的中断系统如何管理这些中断NVIC的结构![请添加图片描述](https://img-blog.csdnimg.cn/c77b038fd63a4ddfbcd3b86f6dfe596b.png) 优先级窗口看门狗&#xff08;WWDG&#xff09;&#xff1a;外部中断模块的特性&#…

[Python] scikit-learn - K近邻算法介绍和使用案例

什么是K近邻算法&#xff1f; K近邻算法(K-Nearest Neighbors,简称KNN)是一种基于实例的学习方法&#xff0c;主要用于分类和回归任务。它的基本思想是&#xff1a;给定一个训练数据集&#xff0c;对于一个新的输入实例&#xff0c;在训练数据集中找到与该实例最邻近的K个实例…

SpringBoot统一返回和统一异常处理

Session 认证和 Token 认证 过滤器和拦截器 SpringBoot统一返回和统一异常处理 上篇文章我们学习了基于 Token 认证的登录功能实现&#xff0c;分别使用了过滤器和拦截器去实现登录功能&#xff0c;这篇文章我们来学习项目中常用的统一返回结果和统一异常处理。 一、统一返…

java.lang.IllegalArgumentException: When allowCredentials is true

1.遇到的错误 java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a…

vue echarts地图

下载地图文件&#xff1a; DataV.GeoAtlas地理小工具系列 范围选择器右侧行政区划范围中输入需要选择的省份或地市&#xff0c;选择自己想要的数据格式&#xff0c;这里选择了geojson格式&#xff0c;点右侧的蓝色按钮复制到浏览器地址栏中&#xff0c;打开的geojson文件内容…

[每日一题] 01.23 - 画矩形

画矩形 height,width,c,d input().split() height,width,d int(height),int(width),int(d) lis [c * width if d else c * (width - 2) c for i in range(height) ]lis: ##### # # # # ##### 或 # # # # # # # #if not d:print(c * width)for i in lis[1:-1…