Redis6.0.6源码阅读——数据淘汰

834 阅读10分钟

前言

Redis是基于内存的Key——Val数据库,可以通过配置设置最大使用内存量,来避免过多占用服务器内存。当可以内存为零的时候,Redis会主动淘汰一些数据来释放内存空间,这篇文章分析一下Redis的主动淘汰数据和被动淘汰数据策略。

正文

Redis中可以对key设置过期时间参数,目前主流删除过期数据有以下三种策略:

  • 主动删除:
    • 定期扫描:每隔一段时间去扫描数据库的过期数据,然后进行删除
    • 定时删除:每次创建过期key的时候设置一个定时器,定时器触发后删除该数据
  • 被动删除:每当访问数据的时候检查是否过期,过期则删除

++定时删除会创建很多定时器浪费CPU资源,Redis主服务是单线程运行的,是不友好的;被动删除浪费内存,因为过期不立刻释放内存++

redis采用了定期扫描和被动删除的结合,服务器启动的时候会运行定时任务,在访问数据以及内存使用满时会主动淘汰数据。

访问时淘汰

在前几篇文章介绍到,当使用get/set方法时候,会用expireIfNeeded来判断key是否过期

int expireIfNeeded(redisDb *db, robj *key) {
    //key未过期
    if (!keyIsExpired(db,key)) return 0;

    //如果是从库只返回装备 不涉及到删除key
    if (server.masterhost != NULL) return 1;

    server.stat_expiredkeys++;
    //命令传播
    propagateExpire(db,key,server.lazyfree_lazy_expire);
    notifyKeyspaceEvent(NOTIFY_EXPIRED,
        "expired",key,db->id);
    //延迟删除还是直接删除
    int retval = server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
                                               dbSyncDelete(db,key);
    if (retval) signalModifiedKey(NULL,db,key);
    return retval;
}

根据lazyfree_lazy_expire来确定数据是否延迟删除

#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
    //首先删除expires字典里面的数据
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
    
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);
        //返回删除对象所需要的量
        size_t free_effort = lazyfreeGetFreeEffort(val);

        //如果对象回收工作量大 放到bio线程回收 设置entry为null
        if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
            atomicIncr(lazyfree_objects,1);
            bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
            dictSetVal(db->dict,de,NULL);
        }
    }
    
    if (de) {
        dictFreeUnlinkedEntry(db->dict,de);
        if (server.cluster_enabled) slotToKeyDel(key->ptr);
        return 1;
    } else {
        return 0;
    }
}

异步删除原理是在删除dict的key-val时候,不对数据进行释放,而是放到一个bio线程里面后台进行

以上是访问数据时进行的过期key删除策略

内存不足时淘汰

当内存不足时,一般采用的淘汰策略是LRU或LFU,前面提到过redisObject上就有lru参数来记录数据

typedef struct redisObject {
    unsigned lru:LRU_BITS;
} robj;

redis的LRU和LFU和传统的算法并不完全一样,传统的算法会维护一个双向链表,数据在链表中移动。Redis由于数据很多,频繁的移动链表节点会影响性能,所以只用了lru来记录信息而已。

对于LRU算法来说lru记录了对象访问时间,但是对于LFU来说lru不仅仅是访问次数的递增,因为在一段时间内频繁被访问的数据,以后可能不那么频繁。

在使用LFU算法的时候会将lru分为两段:

|前16bit|后8bit| |-------|-------|-------| |最近一次计数降低时间|计数|

LFU会随着时间的推移降低计数,为了避免上述情况,从代码中可以看到实现方式。

if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
                updateLFU(val);
            } else {
                val->lru = LRU_CLOCK();
            }

前面提到过 set/get等命令会触发key的lru参数改变

void updateLFU(robj *val) {
    unsigned long counter = LFUDecrAndReturn(val);
    counter = LFULogIncr(counter);
    val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}

首先调用LFUDecrAndReturn来减少计数 然后增加计数


//计算距离上次改变计数时间
unsigned long LFUTimeElapsed(unsigned long ldt) {
    unsigned long now = LFUGetTimeInMinutes();
    if (now >= ldt) return now-ldt;
    return 65535-ldt+now;
}

unsigned long LFUDecrAndReturn(robj *o) {
    unsigned long ldt = o->lru >> 8;
    unsigned long counter = o->lru & 255;
    //根据lfu_decay_time来计算应该 减去多少计数
    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;
}

redis支持以下几种配置来设置内存淘汰策略

  • noeviction 不要淘汰任何数据
  • volatile-random 随机删除设置了过期时间的key
  • allkeys-random 删除随机任何key
  • volatile-ttl 删除最接近到期​​时间(较小的TTL)的键。
  • volatile-lru 使用近似的LRU淘汰过期key
  • allkeys-lru 使用近似的LRU算法淘汰所有key
  • volatile-lfu 在设置了过期时间的键中,使用近似的LFU算法淘汰过期key
  • allkeys-lfu 使用近似的LFU算法淘汰所有key
if (server.maxmemory && !server.lua_timedout) {
        int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;

int freeMemoryIfNeededAndSafe(void) {
    if (server.lua_timedout || server.loading) return C_OK;
    return freeMemoryIfNeeded();
}

关于执行入口是在processCommand中判断的,调用freeMemoryIfNeeded方法

下面一步一步看freeMemoryIfNeeded流程

int freeMemoryIfNeeded(void) {
    int keys_freed = 0;
    //从节点配置了忽略maxmemory
    if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;

    size_t mem_reported, mem_tofree, mem_freed;
    mstime_t latency, eviction_latency, lazyfree_latency;
    long long delta;
    int slaves = listLength(server.slaves);
    int result = C_ERR;

    //客户端暂停返回OK
    if (clientsArePaused()) return C_OK;
    //计算是否还有空闲空间
    if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
        return C_OK;

    mem_freed = 0;

    latencyStartMonitor(latency);
    //不淘汰策略
    if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
        goto cant_free;

开始的部分通过getMaxmemoryState方法计算已使用的空间,以及空闲空间

int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *level) {
    size_t mem_reported, mem_used, mem_tofree;

    //获取zmalloc使用的内存
    mem_reported = zmalloc_used_memory();
    if (total) *total = mem_reported;

    //没有配置maxmemory或者没有达到最大内存
    int return_ok_asap = !server.maxmemory || mem_reported <= server.maxmemory;
    if (return_ok_asap && !level) return C_OK;

    mem_used = mem_reported;
    //获取aofBuf和client的OutputBuffer
    size_t overhead = freeMemoryGetNotCountedMemory();
    mem_used = (mem_used > overhead) ? mem_used-overhead : 0;

    //计算使用率
    if (level) {
        if (!server.maxmemory) {
            *level = 0;
        } else {
            *level = (float)mem_used / (float)server.maxmemory;
        }
    }

    if (return_ok_asap) return C_OK;

    //还没有到达限制
    if (mem_used <= server.maxmemory) return C_OK;

    //需要释放多少空间
    mem_tofree = mem_used - server.maxmemory;

    if (logical) *logical = mem_used;
    if (tofree) *tofree = mem_tofree;

    return C_ERR;
}

通过zmalloc_used_memory方法获取已使用的内存,然后进行计算

size_t zmalloc_used_memory(void) {
    size_t um;
    atomicGet(used_memory,um);
    return um;
}

redis使用自己编写的zmalloc方法分配内存,每次分配成功后会增加used_memory,从而统计出使用内存数

//直到释放足够空间为止
    while (mem_freed < mem_tofree) {
        int j, k, i;
        static unsigned int next_db = 0;
        sds bestkey = NULL;
        int bestdbid;
        redisDb *db;
        dict *dict;
        dictEntry *de;

会不断循环,直到释放空间达到mem_tofree为止,以上是定义所需的数据,每次只选中一个Key

if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
            server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
        {
            struct evictionPoolEntry *pool = EvictionPoolLRU;

LRU、LFU、TTL算法都是放到一起实现的

struct evictionPoolEntry *pool = EvictionPoolLRU;

            while(bestkey == NULL) {
                unsigned long total_keys = 0, keys;

                for (i = 0; i < server.dbnum; i++) {
                    db = server.db+i;
                    //根据策略 ALLKEYS 选择所有key还是仅在过期中选
                    dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
                            db->dict : db->expires;
                    if ((keys = dictSize(dict)) != 0) {
                        evictionPoolPopulate(i, dict, db->dict, pool);
                        total_keys += keys;
                    }
                }
                //没有key选中
                if (!total_keys) break;

                /* Go backward from best to worst element to evict. */
                for (k = EVPOOL_SIZE-1; k >= 0; k--) {
                    if (pool[k].key == NULL) continue;
                    bestdbid = pool[k].dbid;


                    //根据策略 ALLKEYS 选择所有key还是仅在过期中选
                    if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
                        de = dictFind(server.db[pool[k].dbid].dict,
                            pool[k].key);
                    } else {
                        de = dictFind(server.db[pool[k].dbid].expires,
                            pool[k].key);
                    }


                    //删除pool对应key
                    if (pool[k].key != pool[k].cached)
                        sdsfree(pool[k].key);
                    pool[k].key = NULL;
                    pool[k].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 {
                        //key不存在
                    }
                }
            }

开头使用了evictionPoolEntry数组来存放需要淘汰的数据,一次选择16个

#define EVPOOL_SIZE 16
#define EVPOOL_CACHED_SDS_SIZE 255
struct evictionPoolEntry {
    unsigned long long idle;    //LRU代表空闲时间 LFU代表频率
    sds key;                    /* Key name. */
    sds cached;                 /* Cached SDS object for key name. */
    int dbid;                   //数据库id
};

可以从上面一大段代码中得知:

  1. ALLKEYS的区别只在于从普通dict中选择,还是从expires中选择
  2. 通过evictionPoolPopulate筛选数据放到pool中,然后逆序拿出来获取bestkey
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
    int j, k, count;
    dictEntry *samples[server.maxmemory_samples];

    //获取到count个随机元素
    count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
    for (j = 0; j < count; j++) {
        unsigned long long idle;
        sds key;
        robj *o;
        dictEntry *de;

        de = samples[j];
        key = dictGetKey(de);


        //如果不是删除到期时间 那么应该从dict里面获取对象
        if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
            if (sampledict != keydict) de = dictFind(keydict, key);
            o = dictGetVal(de);
        }

        //LRU和LFU计算不同
        if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
            idle = estimateObjectIdleTime(o);
        } else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
            idle = 255-LFUDecrAndReturn(o);
        } else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
           //dictGetVal返回的是expiry数据库过期时间 用最大时间-过期时间 过期时间越短 值越大
            idle = ULLONG_MAX - (long)dictGetVal(de);
        } else {
            serverPanic("Unknown eviction policy in evictionPoolPopulate()");
        }

        //idle排序
        k = 0;
        while (k < EVPOOL_SIZE &&
               pool[k].key &&
               pool[k].idle < idle) k++;
        //无法插入 插入位置是0
        if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
            continue;
        //空槽可以插入
        } else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
        } else {
            //最右边是空的 移动元素腾出空间插入
            if (pool[EVPOOL_SIZE-1].key == NULL) {
                sds cached = pool[EVPOOL_SIZE-1].cached;
                memmove(pool+k+1,pool+k,
                    sizeof(pool[0])*(EVPOOL_SIZE-k-1));
                pool[k].cached = cached;
            } else {
                //没有空位 插入到k-1位 需要丢弃
                k--;
                sds cached = pool[0].cached;
                if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);
                memmove(pool,pool+1,sizeof(pool[0])*k);
                pool[k].cached = cached;
            }
        }

        /* 缓存字符串 */
        int klen = sdslen(key);
        if (klen > EVPOOL_CACHED_SDS_SIZE) {
            pool[k].key = sdsdup(key);
        } else {
            memcpy(pool[k].cached,key,klen+1);
            sdssetlen(pool[k].cached,klen);
            pool[k].key = pool[k].cached;
        }
        pool[k].idle = idle;
        pool[k].dbid = dbid;
    }
}

可以总结如下:

  1. evictionPoolPopulate也是随机选择的,通过dictGetSomeKeys随机选择n个元素,返回实际选择的数量count
  2. pool是排序的,通过LRU或LFU或者TTL的值来排序,最终排序出16个值得淘汰的数据

dictGetSomeKeys方法就不看了,原理就是随机到dict的一个槽,然后顺序收集n个数据为止

/* 随机删除所有/设置过期的key */
        else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
                 server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
        {

            //通过nextDb来选择遍历
            for (i = 0; i < server.dbnum; i++) {
                j = (++next_db) % server.dbnum;
                db = server.db+j;
                dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
                        db->dict : db->expires;
                if (dictSize(dict) != 0) {
                    de = dictGetRandomKey(dict);
                    bestkey = dictGetKey(de);
                    bestdbid = j;
                    break;
                }
            }
        }

随机方法就是真的随机获取一个key

if (bestkey) {
            db = server.db+bestdbid;
            robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
            //传播过期命令
            propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);

            //以下是删除数据 计算释放的空间
            delta = (long long) zmalloc_used_memory();
            latencyStartMonitor(eviction_latency);
            if (server.lazyfree_lazy_eviction)
                dbAsyncDelete(db,keyobj);
            else
                dbSyncDelete(db,keyobj);
            signalModifiedKey(NULL,db,keyobj);
            latencyEndMonitor(eviction_latency);
            latencyAddSampleIfNeeded("eviction-del",eviction_latency);
            delta -= (long long) zmalloc_used_memory();
            mem_freed += delta;
            server.stat_evictedkeys++;
            notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
                keyobj, db->id);
            decrRefCount(keyobj);
            keys_freed++;
            
            if (slaves) flushSlavesOutputBuffers();

            //当使用异步删除的时候 需要检查是否已经释放
            if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
                if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
                    /* Let's satisfy our stop condition. */
                    mem_freed = mem_tofree;
                }
            }
        } else {
            goto cant_free; /* nothing to free... */
        }
    }
    result = C_OK;

真正的删除逻辑,还是分为异步删除和同步删除

cant_free:
    if (result != C_OK) {
        latencyStartMonitor(lazyfree_latency);
        while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
            if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
                result = C_OK;
                break;
            }
            usleep(1000);
        }
        latencyEndMonitor(lazyfree_latency);
        latencyAddSampleIfNeeded("eviction-lazyfree",lazyfree_latency);
    }
    latencyEndMonitor(latency);
    latencyAddSampleIfNeeded("eviction-cycle",latency);
    return result;
}

无法释放内存会sleep主线程

总结:

  • LRU/LFU只是标记上的区别,真正筛选还是随机,有了LRU/LFU/TTL只是让随机的前提下更精确。
  • ALLKEYS的实现只是dict和expires字典的区别,使用ALLKEYS可能会淘汰掉正常数据。

定期删除

前面介绍的两种都是在执行命令的时候才会进行淘汰数据,redis还定期淘汰数据,和前面介绍的定期rehash一样。

void databasesCron(void) {
    /* Expire keys by random sampling. Not required for slaves
     * as master will synthesize DELs for us. */
    if (server.active_expire_enabled) {
        if (iAmMaster()) {
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
        } else {
            expireSlaveKeys();
        }
    }

activeExpireCycle方法定时执行

void activeExpireCycle(int type) {
    unsigned long
    //有效期的活跃度 控制以下参数的
    effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
    //抽样数量
    config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
                           ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
    //任务时间间隔
    config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
                                 ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
    //CPU使用率
    config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
                                  2*effort,
    //抽样删除百分比
    config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
                                    effort;

active_expire_effort是可以自定义的,用于控制以下几个参数的大小,范围1-10

根据active_expire_effort范围计算出来以下几个参数的范围:

  • config_keys_per_loop抽样数量:[25-65]个
  • config_cycle_fast_duration快速时间间隔:[1250-3250]Microseconds
  • config_cycle_slow_time_perc慢扫描CPU使用率:[25-43]%
  • config_cycle_acceptable_stale抽样百分比:[10-1]%
//用静态参数记录最后选择的DB、上次是否中断、上次调用的时间
    static unsigned int current_db = 0;
    static int timelimit_exit = 0;      
    static long long last_fast_cycle = 0; 

    int j, iteration = 0;
    //处理数据库个数默认为16
    int dbs_per_call = CRON_DBS_PER_CALL;
    long long start = ustime(), timelimit, elapsed;

以上是一些参数

if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        //如果上次没有提前退出 且 系统key过期估算的百分比小于当前设置的百分比
        if (!timelimit_exit &&
            server.stat_expired_stale_perc < config_cycle_acceptable_stale)
            return;

        //当前时间 小于 上次任务开始时间+两倍任务时间间隔
        if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2)
            return;

        last_fast_cycle = start;
    }

如果是指定快速扫描,那么以上情况这一次不用扫描

//扫描的次数不应该大于总db数量 如果上次提前退出了要扫描全db
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;


    //通过cpu使用率和当前CPU执行效率来计算 此次收集时间限制
    timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = config_cycle_fast_duration;

    //累积数据
    long total_sampled = 0;
    long total_expired = 0;

dbs_per_call和timelimit的条件初始化

接下来是淘汰过程了

    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        unsigned long expired, sampled;

        redisDb *db = server.db+(current_db % server.dbnum);

        current_db++;

        do {
            unsigned long num, slots;
            // 计算key的平均过期时间
            long long now, ttl_sum;
            int ttl_samples;
            iteration++;

            //没有过期 中断 
            if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
                break;
            }
            // 
            slots = dictSlots(db->expires);
            now = mstime();

            // num/slots 表示已用百分比 该表空间只用了1%不到 则不要扫描这个表 浪费时间
            if (num && slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;
            
            expired = 0;
            sampled = 0;
            ttl_sum = 0;
            ttl_samples = 0;

            if (num > config_keys_per_loop)
                num = config_keys_per_loop;

            //访问最大的次数 是 采样数 * 20
            long max_buckets = num*20;
            long checked_buckets = 0;

            while (sampled < num && checked_buckets < max_buckets) {
                for (int table = 0; table < 2; table++) {
                    //循环从两个表扫描 如果没有rehash 跳过扫描table[1]
                    if (table == 1 && !dictIsRehashing(db->expires)) break;

                    unsigned long idx = db->expires_cursor;
                    idx &= db->expires->ht[table].sizemask;
                    dictEntry *de = db->expires->ht[table].table[idx];
                    long long ttl;

                    //扫描槽上所有节点
                    checked_buckets++;
                    while(de) {
                        dictEntry *e = de;
                        de = de->next;

                        ttl = dictGetSignedIntegerVal(e)-now;
                        //尝试删除过期数据 成功增加expired
                        if (activeExpireCycleTryExpire(db,e,now)) expired++;
                        if (ttl > 0) {
                            ttl_sum += ttl;
                            ttl_samples++;
                        }
                        sampled++;
                    }
                }
                db->expires_cursor++;
            }
            total_expired += expired;
            total_sampled += sampled;

            /* Update the average TTL stats for this database. */
            if (ttl_samples) {
                long long avg_ttl = ttl_sum/ttl_samples;

                //跟新平均值 当前计算的权重只占2%
                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
            }

            //每16个周期迭代一次 超时了现在中断
            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
                elapsed = ustime()-start;
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    server.stat_expired_time_cap_reached_count++;
                    break;
                }
            }
            //如果样本没有检测到过期数据 或者 当前过期暂占比大于阈值 继续下一轮
        } while (sampled == 0 ||
                 (expired*100/sampled) > config_cycle_acceptable_stale);
    }

    elapsed = ustime()-start;
    server.stat_expire_cycle_time_used += elapsed;
    latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);

    /* Update our estimate of keys existing but yet to be expired.
     * Running average with this sample accounting for 5%. */
    double current_perc;
    if (total_sampled) {
        current_perc = (double)total_expired/total_sampled;
    } else
        current_perc = 0;
    //跟新服务器stat_expired_stale_perc current_perc只影响5%
    server.stat_expired_stale_perc = (current_perc*0.05)+
                                     (server.stat_expired_stale_perc*0.95);

根据注释,有以下几点:

  • 会扫描所有的db,每次会计算时间,如果超时中断并且设置标志
  • 会跳过只使用1%不到的表 因为收集这种表得不偿失
  • 会记录当前db扫描出平均过期时间,然后跟新字段,只占2%权重
  • 最后跟新stat_expired_stale_perc(系统key过期估算的百分比),这一次只占5%权重
int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
    long long t = dictGetSignedIntegerVal(de);
    if (now > t) {
        sds key = dictGetKey(de);
        robj *keyobj = createStringObject(key,sdslen(key));

        propagateExpire(db,keyobj,server.lazyfree_lazy_expire);
        if (server.lazyfree_lazy_expire)
            dbAsyncDelete(db,keyobj);
        else
            dbSyncDelete(db,keyobj);
        notifyKeyspaceEvent(NOTIFY_EXPIRED,
            "expired",keyobj,db->id);
        trackingInvalidateKey(NULL,keyobj);
        decrRefCount(keyobj);
        server.stat_expiredkeys++;
        return 1;
    } else {
        return 0;
    }
}

删除过期key方法,还是分为同步或者异步

以上总结:定期删除会根据cpu使用率计算出运行一次所需要的最大时间,然后通过db上面的expires_cursor参数记录删除过期key的位置,在规定的时间范围内尽可能删除过期key,并且会计算过期率。

总结

  • Redis的LRU/LFU主要区别在记录上,实际删除逻辑都是一样的
  • Redis异步删除会将释放内存操作放到主线程以外执行
  • Redis释放内存只能释放key-val内存,对于主从和AOF等缓冲区则不涉及
  • Redis无法释放内存的时候会sleep,这个过程不容易察觉