栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Redis 缓存淘汰策略以及 LRU、LFU 算法

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Redis 缓存淘汰策略以及 LRU、LFU 算法

前言

Redis 使用内存来保存数据,而物理内存是有限的,如果不对 Redis 使用内存做出限制,当内存不够用时,操作系统将通过 swap 分区让数据在内存和硬盘之间来回置换,这会严重影响 Redis 性能,因此我们一般要配置 Redis 可以使用的最大内存(maxmemory)。

maxmemory-policy 淘汰策略

当使用的内存达到设置的最大使用内存时,将会触发内存淘汰策略, Redis 提供以下八种策略:

  • noeviction:不淘汰任何数据,内存溢出时直接返回 OOM 错误信息;
  • volatile-random:随机移除设置了过期时间的 Key;
  • volatile-ttl:针对带有过期时间的 Key,移除离过期时间最近的 Key(越早过期越先移除);
  • volatile-lru:针对带有过期时间的 Key,使用 LRU(Least Recently Used) 算法筛选移除;、volatile-lfu:针对带有过期时间的 Key,使用 LFU(Least Frequently Used) 算法筛选移除;allkeys-random:针对所有 Key 进行随机移除;allkeys-lru:针对所有 Key 使用 LRU(Least Recently Used) 算法移除;allkeys-lfu:针对所有 Key 使用 LFU(Least Frequently Used) 算法移除。

    Redis 中的 LRU 算法

    传统 LRU 算法

    传统的 LRU 算法 把所有的数据组织成一个链表,链表的头和尾分别表示 MRU 端(最近最常使用) 和 LRU 端(最近最不常用)。

    每次访问数据时,把该数据移动到 MRU 端,当缓存没有空间需要删除数据的时候,从 LRU 端选择数据删除,这样就可以把最近访问的数据留在缓存中,删除离当前时间最久没有被访问的数据。

    当然,因为传统的算法需要使用链表来保存所有的数据,同时存在大量的数据移动,因此,Redis 并没有使用这种做法。

    Redis LRU 算法

    Redis 在每个数据对象 RedisObject 中存放 lru 字段,表示该数据最近一次访问的时间戳,以后做数据淘汰时用该字段作为比较依据。

    当执行数据淘汰时, 首次 执行将按以下步骤选择数据:

    1、随机 选出 N (maxmemory-samples)个数据,把它们作为一个候选集合;

    2、比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据淘汰出去;

    以后 再次 进行数据淘汰时,将以 第一次淘汰时创建的候选集合中最小的 lru 值 minLruInSet 为基准,挑选 lru 字段值 小于 minLruInSet 的数据并放入到集合中,当候选数据集中的数据个数再次达到 maxmemory-samples 时,Redis 就把候选集合中 lru 字段值最小的数据淘汰出去。

    通过维护这个 lru 小值集合可以减小发生数据淘汰时对 redis 产生的性能影响,因为它不需要使用链表来保存所有的数据,也不存在数据的移动。

    官网 表明在样本数 maxmemory-samples = 10 的情况下,Redis3.0 很接近真正的 LRU 实现。

    Redis 中的 LFU 算法

    策略

    LRU 算法存在一个缺陷,因为它 只关心数据的访问时间,在发生 扫描式单次查询操作时,所有的数据都会被访问一次,这样可能导致很多热数据反而被排到了 LRU 的末端而被淘汰。

    针对这个问题,LFU 缓存策略 在 LRU 策略基础上,为每个数据 增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时:

    1、首先根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出去;

    2、如果两个数据的访问次数相同,再比较两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出去。

    因此,LFU 算法会将访问更频繁的数据保留,而优先淘汰访问次数最少的数据,在访问次数相当的情况下再选择访问时间最久远的数据淘汰。

    实现

    LFU 在实现上是把原来 24bit 大小的 lru 字段拆成两部分:

  • ldt 值:lru 字段的前 16bit,表示数据的访问时间戳;
  • counter 值:lru 字段的后 8bit,表示数据的访问次数。

    8 bit 记录访问次数,最多只能到 255。如果访问一次就加 1 的话,可能大部分数据都会很快达到这个值,那这个值将失去意义。因此,Redis 使用了一个增长更慢的计数规则:

    double r = (double)rand()/RAND_MAX;
    double p = 1.0/(baseval*server.lfu_log_factor+1);
    if (r < p) 
        counter++;

  • r 是一个取值范围在(0,1)间的随机数;
  • baseval 是计数器的当前值;
  • lfu_log_factor 为计数器增长因子。

    每当数据被访问时,首先,用 baseval 乘以 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;

    然后,把这个 p 值和 r 值作比较, p 值大于 r 值时计数器加 1。

    官网也提供了 lfu_log_factor 不同取值时的计数器增长情况:

    +--------+------------+------------+------------+------------+------------+
    | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
    +--------+------------+------------+------------+------------+------------+
    | 0      | 104        | 255        | 255        | 255        | 255        |
    +--------+------------+------------+------------+------------+------------+
    | 1      | 18         | 49         | 255        | 255        | 255        |
    +--------+------------+------------+------------+------------+------------+
    | 10     | 10         | 18         | 142        | 255        | 255        |
    +--------+------------+------------+------------+------------+------------+
    | 100    | 8          | 11         | 49         | 143        | 255        |
    +--------+------------+------------+------------+------------+------------+

    此外,Redis 还设计了一个 counter 值的衰减机制,这主要是为了能够淘汰那些可能短时间内被频繁访问,但是之后不再需要的数据。

    unsigned long LFUDecrAndReturn(robj *o) {
      // 获取lru的高16位,也就是ldt
      unsigned long ldt = o->lru >> 8;  
      // 获取lru的低8位,也就是logc  
      unsigned long counter = o->lru & 255;
      // 根据配置的lfu-decay-time计算Logistic Counter需要衰减的值
      unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
      if (num_periods)
        counter = (num_periods > counter) ? 0 : counter - num_periods;
      return counter;
    }
    
    // 计算距离上次访问的间隔时间
    unsigned long LFUTimeElapsed(unsigned long ldt) {
        // 取当前时间戳(单位:分钟)
        unsigned long now = LFUGetTimeInMinutes();
        // 计算时间差
        if (now >= ldt) 
            return now-ldt;
        return 65535-ldt+now;
    }
    
    // 获取当前时间戳,以分钟为单位,取低 8 位
    unsigned long LFUGetTimeInMinutes(void) {
        return (server.unixtime/60) & 65535;
    }
    

    LFU 策略使用衰减因子配置项 lfu_decay_time 来控制访问次数的衰减。

    因为数据对象只有前 16 位用来保存时间戳,所以只能比较到分钟级别,在计算的时候:

    首先把当前时间换成以分为单位,然后计算 其与 ldt 的差值,再把这个差值除以 lfu_decay_time ,所得的结果就是数据 counter 要衰减的值。

    相关配置

  • maxmemory :最大使用内存
  • maxmemory-policy :淘汰策略
  • maxmemory-samples: LRU 算法中采样集合大小
  • lfu_log_factor:计数器增长因子
  • lfu-decay-time:LFU 算法计数器衰减因子

    以上参数可以通过配置文件 redis.conf 配置,也可以通过命令行 config set [key] [value] 设置。

  • 转载请注明:文章转载自 www.mshxw.com
    本文地址:https://www.mshxw.com/it/764141.html
    我们一直用心在做
    关于我们 文章归档 网站地图 联系我们

    版权所有 (c)2021-2022 MSHXW.COM

    ICP备案号:晋ICP备2021003244-6号