开启掘金成长之旅!这是我参与「掘金日新计划 · 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 天,点击查看活动详情