除了主动淘汰的频率外,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); } }