• Redis LRU源码分析


    LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”,通常是使用双向链表来实现,会占用大量内存,所以redis使用的是近似LRU的算法,即每次访问时,给该对象记录一下当前的时间戳(单位秒),当需要删除数据时,随机选取5个元素,删除最久未被访问的。

    下面先看下redis object的定义(基于redis 2.8.8版本源代码):

    /* The actual Redis Object */
    #define REDIS_LRU_BITS 24
    #define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1) /* Max value of obj->lru */
    #define REDIS_LRU_CLOCK_RESOLUTION 1 /* LRU clock resolution in seconds */
    typedef struct redisObject {
        unsigned type:4;
        unsigned encoding:4;
        unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
        int refcount;
        void *ptr;
    } robj;

    其中“REDIS_LRU_CLOCK_RESOLUTION”是LRU算法的精度,默认1S,数字越低精度越高,精度越高,lru时间溢出时间越短,下面细看。

    其中的“lru”记录最后一次访问的时间戳,由于只有24位,无法记录完整的时间,因此只记录了unix time的低24位,24 bits数据要溢出的话需要194天,而缓存的数据更新非常频繁,已经足够了。

    下面看下创建redis对象时的代码:

    robj *createObject(int type, void *ptr) {
        robj *o = zmalloc(sizeof(*o));
        o->type = type;
        o->encoding = REDIS_ENCODING_RAW;
        o->ptr = ptr;
        o->refcount = 1;
    
        /* Set the LRU to the current lruclock (minutes resolution). */
        o->lru = server.lruclock;
        return o;
    }

    我们看到是直接把server.lruclock赋值给lru,而server.lruclock会由redis的后台线程每秒更新一次,其他需要的地方直接引用,减少unix time访问,节省资源,代码如下:

    void updateLRUClock(void) {
        server.lruclock = (server.unixtime/REDIS_LRU_CLOCK_RESOLUTION) &
                                                    REDIS_LRU_CLOCK_MAX; // 把unix time的低24位赋值给server.lruclock
    }

    当后续再访问redis对象时,更新lru字段,如下:

    /*-----------------------------------------------------------------------------
     * C-level DB API
     *----------------------------------------------------------------------------*/
    
    robj *lookupKey(redisDb *db, robj *key) {
        dictEntry *de = dictFind(db->dict,key->ptr);
        if (de) {
            robj *val = dictGetVal(de);
    
            /* Update the access time for the ageing algorithm.
             * Don't do it if we have a saving child, as this will trigger
             * a copy on write madness. */
            if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) // 在执行rdb任务或者aof任务时,不能更新该字段
                val->lru = server.lruclock;
            return val;
        } else {
            return NULL;
        }
    }

    下面再看下LRU策略触发时,是如何选择对象的:

     /* volatile-lru and allkeys-lru policy */
                else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                    server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
                {
                    for (k = 0; k < server.maxmemory_samples; k++) { //  server.maxmemory_samples 默认是5
                        sds thiskey;
                        long thisval;
                        robj *o;
    
                        de = dictGetRandomKey(dict);   // 随机选择一个对象
                        thiskey = dictGetKey(de);
                        /* When policy is volatile-lru we need an additional lookup
                         * to locate the real key, as dict is set to db->expires. */
                        if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
                            de = dictFind(db->dict, thiskey);
                        o = dictGetVal(de);
                        thisval = estimateObjectIdleTime(o);  // 获取对象的lru值
    
                        /* Higher idle time is better candidate for deletion */
                        if (bestkey == NULL || thisval > bestval) {
                            bestkey = thiskey;
                            bestval = thisval;
                        }
                    }
                }
    /* Given an object returns the min number of seconds the object was never
     * requested, using an approximated LRU algorithm. */
    unsigned long estimateObjectIdleTime(robj *o) {  // 计算对象距离上次访问流逝的时间,单位秒
        if (server.lruclock >= o->lru) {
            return (server.lruclock - o->lru) * REDIS_LRU_CLOCK_RESOLUTION;
        } else {  // o->lru > server.lruclock,说明距离上次访问已经至少超过一个REDIS_LRU_CLOCK_MAX了(194天)
            return ((REDIS_LRU_CLOCK_MAX - o->lru) + server.lruclock) *
                        REDIS_LRU_CLOCK_RESOLUTION;
        }
    }

    从上面的代码可以看出,redis的lru近似算法有三个要点:

    1. 守护线程,每秒更新一次server.lruclock值(unix time的低24位)。

    2. redis对象首次创建和后续访问时,把当前server.lruclock赋值给该对象的lru。

    3. redis在处理命令时,会检查内存使用情况,如果超过限制且配置了LRU策略,则:

        a). 随机选择5个元素。

        b). 删除其中最久未被访问的元素。

        c). 已使用内存是否还是超过限制,如果是则跳转到步骤a继续,否则本次删除结束。

  • 相关阅读:
    面向对象七大基本原则
    JS面向对象的七大基本原则(里氏替换/依赖倒置)
    JS面向对象的七大基本原则(单一/开闭)
    flex均匀布局
    Vue在IDEA的简约安装
    事务及数据表设计
    关于序列化与反序列化
    SQL查询关键字用法
    IO流
    cookie与session
  • 原文地址:https://www.cnblogs.com/xinghebuluo/p/redis.html
Copyright © 2020-2023  润新知