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的过期