理解Redis的LFU算法源码实现

289 阅读4分钟

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

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈


前言

LFU(Least Frequently Used) 算法其实就是根据数据的访问频次筛选出最少被访问的数据进行删除。

Redis使用的内存将超过maxmemory时,Redis会根据maxmemory_policy淘汰策略来决定将哪些数据淘汰掉。使用到LFU算法的淘汰策略如下所示。

  1. volatile-lfu。对设置了过期时间的数据生效,使用LFU算法删除数据;
  2. allkeys-lfu。对所有数据生效,使用LFU算法删除数据。

本篇文章将对Redis 6.0.16版本中的LFU算法实现原理进行分析。

正文

一. LFU算法的时效性问题

如果Redis采用LFU算法作为淘汰策略,那么redisObejct.lru字段有如下含义。

  • 高16位表示redisObejct最近一次被使用的分时间的后16位;
  • 低8位表示redisObejct被使用的次数。

采用LFU算法作为淘汰策略时,会优先淘汰使用次数低的key,但是只按照这样的策略淘汰key的话会引入时效性问题,即使用次数很高但是实际已经很久没有被使用过的数据不容易被淘汰,所以Redis为了解决这个问题,会根据key没有被使用的分钟数减少key的使用次数。

二. 源码分析

当采用LFU算法并需要淘汰key时,会在evict.c文件的evictionPoolPopulate() 方法中调用LFUDecrAndReturn() 方法来获取key的使用次数,并且在LFUDecrAndReturn() 方法中会先根据key没有被使用的分钟数减少key的使用次数,然后将减少后的次数返回。LFUDecrAndReturn() 方法实现如下所示。

unsigned long LFUDecrAndReturn(robj *o) {
    // 获取redisObject.lru的高16位,即最近一次被使用的分时间的后16位
    unsigned long ldt = o->lru >> 8;
    // 获取redisObject.lru的低8位,即使用次数
    unsigned long counter = o->lru & 255;
    // num_periods代表需要减少的使用次数
    // lfu_decay_time是可配的,默认值是1
    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
    if (num_periods)
        // 使用次数最低只能被减少为0
        counter = (num_periods > counter) ? 0 : counter - num_periods;
    return counter;
}

LFUDecrAndReturn() 方法中会使用LFUTimeElapsed() 方法来计算key没有被使用的分钟数,源码如下所示。

unsigned long LFUTimeElapsed(unsigned long ldt) {
    // 获取当前系统分时间的后16位
    unsigned long now = LFUGetTimeInMinutes();
    // 如果当前系统分时间的后16位大于redisObject.lru的后16位,则直接相减
    if (now >= ldt) return now-ldt;
    // 否则65535 - ldt + now
    return 65535-ldt+now;
}

最后需要说明,redisObject.lru的低8位实际上是一个对数计数器,其最大值为255但是不代表实际使用次数为255。实现原理见LFULogIncr() 方法,源码如下所示。

#define LFU_INIT_VAL 5

uint8_t LFULogIncr(uint8_t counter) {
    // 如果当前次数已经为255,则直接返回255
    if (counter == 255) return 255;
    // r是0到1之间的随机数
    double r = (double)rand()/RAND_MAX;
    // LFU_INIT_VAL默认为5,定义在server.h中
    double baseval = counter - LFU_INIT_VAL;
    // 如果当前counter小于LFU_INIT_VAL,则baseval置为0
    if (baseval < 0) baseval = 0;
    // baseval越大,p越小
    double p = 1.0/(baseval*server.lfu_log_factor+1);
    // p越小,counter++的几率越小
    if (r < p) counter++;
    return counter;
}

LFULogIncr() 方法中增加key的使用次数的逻辑可以概括如下。

  • 如果key的当前使用次数小于等于5,则本次使用一定可以让使用次数加1;
  • 如果key的当前使用次数已经为255,则本次使用不增加使用次数;
  • 如果key的当前使用次数大于5,小于255,则当前使用次数越大,本次访问让使用次数加1的概率越小。

三. 流程图

整体的LFU算法的一个流程图如下所示。

Redis-LFU算法流程图


大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈

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