redis 数据删除策略和逐出算法的问题小结

2020-06-12 15:00:04王旭

databasesCron()

databasesCron() 中 调用了 activeExpireCycle()方法,来对过期的数据进行处理。(在这里还会做一些其他操作~ 调整数据库大小,主动和渐进式rehash)

// 对数据库执行删除过期键,调整大小,以及主动和渐进式 rehash
void databasesCron(void) {

 // 判断是否是主服务器 如果是 执行主动过期键清除
 if (server.active_expire_enabled && server.masterhost == NULL)
 // 清除模式为 CYCLE_SLOW ,这个模式会尽量多清除过期键
 activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);

 // 在没有 BGSAVE 或者 BGREWRITEAOF 执行时,对哈希表进行 rehash
 if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {
 static unsigned int resize_db = 0;
 static unsigned int rehash_db = 0;
 unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
 unsigned int j;

 /* Don't test more DBs than we have. */
 // 设定要测试的数据库数量
 if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;

 /* Resize */
 // 调整字典的大小
 for (j = 0; j < dbs_per_call; j++) {
 tryResizeHashTables(resize_db % server.dbnum);
 resize_db++;
 }

 /* Rehash */
 // 对字典进行渐进式 rehash
 if (server.activerehashing) {
 for (j = 0; j < dbs_per_call; j++) {
 int work_done = incrementallyRehash(rehash_db % server.dbnum);
 rehash_db++;
 if (work_done) {
  /* If the function did some work, stop here, we'll do
  * more at the next cron loop. */
  break;
 }
 }
 }
 }
}

activeExpireCycle()

大致流程如下

1 遍历指定个数的db(默认的 16 )进行删除操作

2 针对每个db随机获取过期数据每次遍历不超过指定数量(如20),发现过期数据并进行删除。

3 如果有多于25%的keys过期,重复步骤 2

除了主动淘汰的频率外,Redis对每次淘汰任务执行的最大时长也有一个限定,这样保证了每次主动淘汰不会过多阻塞应用请求,以下是这个限定计算公式:

#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* CPU max % for keys collection */ ``... ``timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;

也就是每次执行时间的25%用于过期数据删除。

void activeExpireCycle(int type) {
 // 静态变量,用来累积函数连续执行时的数据
 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. */

 unsigned int j, iteration = 0;
 // 默认每次处理的数据库数量
 unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
 // 函数开始的时间
 long long start = ustime(), timelimit;

 // 快速模式
 if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
 // 如果上次函数没有触发 timelimit_exit ,那么不执行处理
 if (!timelimit_exit) return;
 // 如果距离上次执行未够一定时间,那么不执行处理
 if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
 // 运行到这里,说明执行快速处理,记录当前时间
 last_fast_cycle = start;
 }

 /* 
 * 一般情况下,函数只处理 REDIS_DBCRON_DBS_PER_CALL 个数据库,
 * 除非:
 *
 * 1) 当前数据库的数量小于 REDIS_DBCRON_DBS_PER_CALL
 * 2) 如果上次处理遇到了时间上限,那么这次需要对所有数据库进行扫描,
 * 这可以避免过多的过期键占用空间
 */
 if (dbs_per_call > server.dbnum || timelimit_exit)
 dbs_per_call = server.dbnum;

 // 函数处理的微秒时间上限
 // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默认为 25 ,也即是 25 % 的 CPU 时间
 timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
 timelimit_exit = 0;
 if (timelimit <= 0) timelimit = 1;

 // 如果是运行在快速模式之下
 // 那么最多只能运行 FAST_DURATION 微秒 
 // 默认值为 1000 (微秒)
 if (type == ACTIVE_EXPIRE_CYCLE_FAST)
 timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */

 // 遍历数据库
 for (j = 0; j < dbs_per_call; j++) {
 int expired;
 // 指向要处理的数据库
 redisDb *db = server.db+(current_db % server.dbnum);

 // 为 DB 计数器加一,如果进入 do 循环之后因为超时而跳出
 // 那么下次会直接从下个 DB 开始处理
 current_db++;

 do {
 unsigned long num, slots;
 long long now, ttl_sum;
 int ttl_samples;

 /* If there is nothing to expire try next DB ASAP. */
 // 获取数据库中带过期时间的键的数量
 // 如果该数量为 0 ,直接跳过这个数据库
 if ((num = dictSize(db->expires)) == 0) {
 db->avg_ttl = 0;
 break;
 }
 // 获取数据库中键值对的数量
 slots = dictSlots(db->expires);
 // 当前时间
 now = mstime();

 // 这个数据库的使用率低于 1% ,扫描起来太费力了(大部分都会 MISS)
 // 跳过,等待字典收缩程序运行
 if (num && slots > DICT_HT_INITIAL_SIZE &&
 (num*100/slots < 1)) break;

 /* 
 * 样本计数器
 */
 // 已处理过期键计数器
 expired = 0;
 // 键的总 TTL 计数器
 ttl_sum = 0;
 // 总共处理的键计数器
 ttl_samples = 0;

 // 每次最多只能检查 LOOKUPS_PER_LOOP 个键
 if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
 num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

 // 开始遍历数据库
 while (num--) {
 dictEntry *de;
 long long ttl;

 // 从 expires 中随机取出一个带过期时间的键
 if ((de = dictGetRandomKey(db->expires)) == NULL) break;
 // 计算 TTL
 ttl = dictGetSignedIntegerVal(de)-now;
 // 如果键已经过期,那么删除它,并将 expired 计数器增一
 if (activeExpireCycleTryExpire(db,de,now)) expired++;
 if (ttl < 0) ttl = 0;
 // 累积键的 TTL
 ttl_sum += ttl;
 // 累积处理键的个数
 ttl_samples++;
 }

 /* Update the average TTL stats for this database. */
 // 为这个数据库更新平均 TTL 统计数据
 if (ttl_samples) {
 // 计算当前平均值
 long long avg_ttl = ttl_sum/ttl_samples;
 
 // 如果这是第一次设置数据库平均 TTL ,那么进行初始化
 if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
 /* Smooth the value averaging with the previous one. */
 // 取数据库的上次平均 TTL 和今次平均 TTL 的平均值
 db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
 }

 // 我们不能用太长时间处理过期键,
 // 所以这个函数执行一定时间之后就要返回

 // 更新遍历次数
 iteration++;

 // 每遍历 16 次执行一次
 if ((iteration & 0xf) == 0 && /* check once every 16 iterations. */
 (ustime()-start) > timelimit)
 {
 // 如果遍历次数正好是 16 的倍数
 // 并且遍历的时间超过了 timelimit
 // 那么断开 timelimit_exit
 timelimit_exit = 1;
 }

 // 已经超时了,返回
 if (timelimit_exit) return;

 // 如果已删除的过期键占当前总数据库带过期时间的键数量的 25 %
 // 那么不再遍历
 } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
 }
}