redis-定期删除策略实现

456 阅读3分钟

redis定期删除策略实现

参考链接

过期键定期删除策略由 expirec.c/activeExpireCycle 实现,每当redis有周期性操作server.c/serverCron,函数执行时,activeExpireCycle 就会调用,

在规定的时间内,遍历db,从 expires 字典中随机抽样,并检查是否过期需要删除。

activeExpireCycle的大致工作模式

  • 每次都是循环16次 ,并且 timelimit_exit 没有超时,从db的 expires字典中每次最多随机抽样20个,判断是否过期,如果过期则删除
  • 如果该db有过期的keys 大于 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4(20/4),则继续从该db中抽样。直到不满足或者 超时
  • current_db 是个 static变量,只会初始化一次,每次调用,都会接着上一次的current_db开始

activeExpireCycle

代码分析

  • 常量值
#define ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 20 /* Loopkups per loop. */
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds */
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* CPU max % for keys collection */
#define ACTIVE_EXPIRE_CYCLE_SLOW 0
#define ACTIVE_EXPIRE_CYCLE_FAST 1

#define CRON_DBS_PER_CALL 16

#define DICT_HT_INITIAL_SIZE     4
  • 初始化
void activeExpireCycle(int type) {
    /* This function has some global state in order to continue the work
     * incrementally across calls. */
    // 静态变量,保留函数上一次调用结束变量的状态,便于下一次接着开始操作。
    static unsigned int current_db = 0; /* Last DB tested. */
    static int timelimit_exit = 0;      /* Time limit hit in previous call? */
    static long long last_fast_cycle = 0; /* When last fast cycle ran. */

    int j, iteration = 0;
    int dbs_per_call = CRON_DBS_PER_CALL; // 16
    long long start = ustime(), timelimit, elapsed;

    if (clientsArePaused()) return;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST) { 
        if (!timelimit_exit) return; // 上次是因为超时的原因,则退出
        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return; //小于时间间隔的则退出,当前时间要小于 上一次fast时间 + 2000us
        last_fast_cycle = start;
    }

    if (dbs_per_call > server.dbnum || timelimit_exit) // 大于 或者 是因为超时原因(可能过期的key比较多,导致超时)则直接使用 server.dbnum
        dbs_per_call = server.dbnum;
		
  	// 计算 timelimit的 时间限制
    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;
    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
		
    long total_sampled = 0;
    long total_expired = 0;
    /* ....          ... ....*/
}
  • 循环 dbs_per_call 操作
void activeExpireCycle(int type) {
  	
  	/*.................*/

    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);
				
        current_db++; // 这个变量是个 静态变量,只会初始化一次。

        /* Continue to expire if at the end of the cycle more than 25%
         * of the keys were expired. */
        do {
            if ((num = dictSize(db->expires)) == 0) { // 当前db没有过期的keys,则下一个db
                db->avg_ttl = 0;
                break;
            }

						
          	// hash的槽的使用率不足1%,则不操作当前db,等之后的rehahs后才操作。避免低效操作。(命中率低,大量的hahs冲突,处理起来浪费时间)
            if (num && slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;	

            if ((iteration & 0xf) == 0) {  // 没16次检查一次,是否超时,超时则 设置超时标志位timelimit_exit=1,并退出循环操作
                elapsed = ustime()-start;
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    server.stat_expired_time_cap_reached_count++;
                    break;
                }
            }
            /* We don't repeat the cycle if there are less than 25% of keys
             * found expired in the current DB. */
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); // 小于25%则退出该db循环操作
    }
  	/*......code code .......*/
}
  • 删除过期keys
void activeExpireCycle(int type) {
  
    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);
        current_db++;
        do {
            if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
                break;
            }
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) // 设置 最大允许抽取的随机数量,最大不超过20个
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

            while (num--) { // 循环,并判断是否过期,并删除
                dictEntry *de;
                long long ttl;
								
              	// 随机获取key,如果该key的过期了则删除,并 expired++
                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                ttl = dictGetSignedIntegerVal(de)-now;
                if (activeExpireCycleTryExpire(db,de,now)) expired++; 
                if (ttl > 0) {
                    /* We want the average TTL of keys yet not expired. */
                    ttl_sum += ttl;
                    ttl_samples++;
                }
                total_sampled++;
            }
            total_expired += expired;
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
}
  • 结尾(计算过期键的占比)
void activeExpireCycle(int type) {
    elapsed = ustime()-start;
    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;
    server.stat_expired_stale_perc = (current_perc*0.05)+
                                     (server.stat_expired_stale_perc*0.95);
}

核心逻辑

  • 外层for条件 dbs_per_call次 && 没有超时,则计算当前 redisdb

    • 当前db的expires字典是否有过期的key,没有则下一个db
    • 计算一下hash槽的可使用率,如果小于1%,则下一个db,(可使用lv低代表,有大量的hash冲突,遍历可能到导致性能变慢,等到一次该字典的rehash)
    • 设置允许的最大抽样的样本个数20,num
      • 从当前db中的 expires随机获取 num个,如果key过期则删除,并记录过期key的个数 expired
    • 如果 迭代次数%16==0,则计算是否超时,如果超时则设置 timelimit_exit=1,并退出循环
    • 如果 expired次数 > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4(20/4) 则代表该db的 expires字典中有大量过期的keys,则继续从该db的expires中抽样删除
  • 计算过期键的占比

Q&A

  • redis可以指定使用的 过期的策略吗?

不可以,代码中写死

  • 定期删除的类型-ACTIVE_EXPIRE_CYCLE_FAST,如何理解?

满足一定的前置条件,快速退出方法,

  • 是否存在,内存够用,定期删除策略时没有被删除掉,继续保留在内存中?

可能会从在,删除的不及时,可以调整reids的配置文件 server.conf的hz 或者 使用 scan 主动触发keys的过期