理解Redis的过期淘汰源码实现

446 阅读5分钟

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

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


前言

Redis中,可以使用expire指令为存储的数据设置过期时间,那么如果有数据过期,Redis会根据过期策略来删除这些过期数据,Redis中的过期策略如下所示。

过期策略说明
定时删除每个设置了过期时间的数据都有一个定时器,一旦数据过期,该数据会立即被删除。优点:过期数据可以及时被删除;缺点:过期数据多时定时器会占用较多CPU资源
惰性删除使用数据的时候才去判断该数据是否过期,如果过期就删除该数据。优点:过期数据被访问时才会被删除,删除过期数据不会占用过多CPU资源;缺点:有些已经过期但是没有被访问的数据会长期得不到删除
定期删除每隔一段时间扫描过期数据并删除。优点:通过控制扫描的间隔时间和执行时间,可以减少删除过期数据的操作对CPU资源的占用;缺点:扫描的间隔时间和执行时间难以确定一个合理值

Redis中是采用惰性删除定期删除来处理过期数据的。本篇文章主要是结合Redis 6.0.16版本源码对定期删除的逻辑进行分析。

正文

首先是server.c文件,过期扫描的调用链如下所示。

serverCron() -> databasesCron() -> activeExpireCycle()

上述的activeExpireCycle() 方法的实现在expire.c文件中,**activeExpireCycle()**方法里有过期删除的主体实现。

activeExpireCycle() 方法比较长,下面仅给出和过期删除机制相关的源码实现,其余部分做适当删除。

#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10

void activeExpireCycle(int type) {

    ......  
                   
    // active_expire_effort配置项默认为1
    // effort默认情况下为0
    effort = server.active_expire_effort-1,
    // config_keys_per_loop默认情况下为20
    config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
                           ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,

    ......

    // config_cycle_slow_time_perc默认情况下为25
    config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
                                  2*effort,
    // config_cycle_acceptable_stale默认情况下为10
    config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
                                    effort;

    ......

    // 计算本次过期扫描允许花费的时间,单位:微秒
    timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;

    ......

    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        // expired表示每次删除的key数量
	// sampled表示每次迭代时抽样到的key数量
        unsigned long expired, sampled;

        ......

        // 开始本次过期扫描的迭代
        do {

            ......

            // 记录迭代次数
            iteration++;

            ......

            // 每次迭代都将expired和sampled置为0
            expired = 0;
            sampled = 0;

            ......

	    // num默认情况下为20
            if (num > config_keys_per_loop)
                num = config_keys_per_loop;

            // max_buckets默认情况下为400
            long max_buckets = num*20;
            // 记录本次迭代已经抽样过的hash桶数量
            long checked_buckets = 0;

	    // 下面这个循环会以hash桶为维度去抽样key
            // 每抽样到一个key就会去判断其是否过期,同时更新expired和sampled
            while (sampled < num && checked_buckets < max_buckets) {
                for (int table = 0; table < 2; table++) {
		     // 没有在rehash时,只会扫描ht[0]
                    if (table == 1 && !dictIsRehashing(db->expires)) break;

                    unsigned long idx = db->expires_cursor;
                    idx &= db->expires->ht[table].sizemask;
		    // 获取hash桶
                    dictEntry *de = db->expires->ht[table].table[idx];
                    long long ttl;
                    
                    // 下面这个循环会抽样hash桶里的所有key
                    checked_buckets++;
                    while(de) {
                        dictEntry *e = de;
                        de = de->next;

                        ttl = dictGetSignedIntegerVal(e)-now;
                        // 判断key是否过期
                        if (activeExpireCycleTryExpire(db,e,now)) expired++;
                        if (ttl > 0) {
                            ttl_sum += ttl;
                            ttl_samples++;
                        }
                        sampled++;
                    }
                }
                db->expires_cursor++;
            }
            
            ......

            // 每次过期扫描时,迭代次数每达到16次就判断一下扫描花费的时间是否大于允许花费的时间
            if ((iteration & 0xf) == 0) {
                elapsed = ustime()-start;
		// 如果扫描花费时间超过了允许花费的时间,结束本次过期扫描
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    server.stat_expired_time_cap_reached_count++;
                    break;
                }
            }
	// 如果本次迭代抽样到的key数量为0,则开启下一次迭代
	// 如果本次迭代删除的key数量占抽样的key数量的占比大于config_cycle_acceptable_stale,则开启下一次迭代
        } while (sampled == 0 ||
                 (expired*100/sampled) > config_cycle_acceptable_stale);
    }

    ......

}

那么结合上述源码分析,可以得到过期扫描规则如下所示。

  • 过期扫描频率为10hz,即每100ms执行一次过期扫描;
  • 过期扫描只会针对设置了过期时间的key进行扫描;
  • 每一次过期扫描中,会进行若干次迭代,每次迭代是以hash桶的维度去抽样key,如果某个hash桶被抽样到,那么hash桶里的所有key都会被抽样到;
  • 每次迭代时抽样到的key如果大于等于20,则本次迭代不再继续抽样其它key
  • 每次迭代时抽样过的hash桶如果大于等于400,则本次迭代不再继续抽样其它hash桶;
  • 每次迭代时,会判断每个被抽样到的key是否过期,如果过期则删除key
  • 每次迭代时,如果删除的key数量占被抽样的key数量的比例大于10%,则开启下一次迭代;
  • 一次过期扫描中,迭代次数每达到16次时就会判断本次过期扫描花费的时间是否大于允许花费的时间,如果大于,则直接结束本次过期扫描。

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

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