首先看下redisDb的结构,在server.h文件中:
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
redis中可以使用以下命令设置key的过期时间
命令很好理解,P开头的都是以毫秒为单位,相关代码实现也很简单。以上四条命令的实现在expire.c
文件中:
/* EXPIRE key seconds */
void expireCommand(client *c) {
expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
/* EXPIREAT key time */
void expireatCommand(client *c) {
expireGenericCommand(c,0,UNIT_SECONDS);
}
/* PEXPIRE key milliseconds */
void pexpireCommand(client *c) {
expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
}
/* PEXPIREAT key ms_time */
void pexpireatCommand(client *c) {
expireGenericCommand(c,0,UNIT_MILLISECONDS);
}
可以看出以上命令都是调用同一个函数实现的,也就是说这四个设置命令之间是可以互相转换的。mstime()
是获取当前秒级时间戳的函数,也就是说,EXPIRE、PEXPIRE,EXPIREAT命令,最终其实都是执行PEXPIREAT命令。
这两个命令的实现也在expire.c
中,ttlCommand和pttlCommand,实现很简单,这里伪代码描述:
if(key不存在):
return -2
if(key没设置过期时间):
return -1
ttl = 计算过期时间()
return 过期时间
键过期了如何删除?一般有以下三种方法:
第一种方式优点是删除快,对内存友好,但是频繁的执行对cpu不友好;
第二中方式对cpu友好,对内存不友好
第三种方式是一、二的折中,当然如果执行的太频繁,或者每次删除的过多,就跟方式一没什么区别。
redis使用了惰性删除和定期删除两种方式。
惰性实现在db.c/expireIfNeeded()中。逻辑很简单:
这里需要注意的就是redis只会从主库删除过期键,主库再同步从库。
redis会定期执行过期键删除,定期删除函数在expire.c/activeExpireCycle(int),这个函数有两个地方调用,一个是server.c/databasesCron()函数,一个是server.c/beforeSleep()函数,databasesCron函数负责处理redis的一些后台操作,例如过期键的处理,调整大小,重散列等操作。
activeExpireCycle()有两种模式,ACTIVE_EXPIRE_CYCLE_FAST和ACTIVE_EXPIRE_CYCLE_SLOW,代表不同的执行周期,前者为“快周期”,后者“慢周期”
为了继续探究redis的删除策略,需要看activeExpireCycle(int)这个函数的实现。
首先先看下代码中对activeExpireCycle函数的注释, 翻译出来大概是以下几个点:
翻译完我们清楚了这个函数大致的执行方式,接下来就看一些细节。
首先看下代码中先定义的一些默认常量:
#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* 默认每个数据库检查的键数量. */
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* “快周期”的周期时间,单位微秒. */
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* "慢周期“做多可占用的cpu资源,% */
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /*可容忍的过期键所占内存百分比 */
从这些默认值可以看出,默认过期键数量不能超过内存的10%,并避免消耗超过25%的cpu资源。
这些只是一些系统默认的常量,redis给了我们一个额外的参数effort,让我们去修改上面这些默认变量。effort变量由redis.conf中的active-expire-effort控制,1到10,设置越大cpu的消耗也会相应增加。
可以通过下面这段代码更清楚的看出effort这个变量的作用
effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
2*effort,
config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
effort;
effort改变了过期键可占内存的最大百分比,改变了最大占用cpu的百分比等,effort越大,cpu负担越重,所以根据自己的需要设置effort的值。
我们可以看到代码中设置了几个静态变量,用于记录每个周期结束时的一些东西。
static unsigned int current_db = 0; /* 上个周期结束时执行到的db */
static int timelimit_exit = 0; /* 上一个周期是否因为超时而退出? */
static long long last_fast_cycle = 0; /* 上一个”快周期“的运行时间 */
这里为了看得清楚,用伪代码表示:
//如果上次不是因为超时而结束,并且当前过期键数量小于可容忍的过期键数量,不处理
if(type == ACTIVE_EXPIRE_CYCLE_FAST):
if(timelimit_exit && server.stat_expired_stale_perc < config_cycle_acceptable_stale) :
return;
//如果距离上次fast模式的运行时间小于两倍的fast模式的周期,不处理
if(start < last_fast_cycle + (long long)config_cycle_fast_duration*2) :
return;
//以上检查全过,那么就可以开始fast模式,记录一下本次的运行时间
last_fast_cycle = start
//如果服务器的db数量比默认的db检查数量小,以服务器的数量为准
//如果上次是超时退出,也会在检查一遍过期键数量
if(dbs_per_call > server.dbnum || timelimit_exit) :
dbs_per_call = server.dbnum;
//开始处理每个db中的过期键
for i in range(dbs_per_call):
//获取当前的db,并将currentdb的索引加一
redisDb = server.db[current_db]
current_db += 1
//如果当先过期键的数量大于可容忍的过期键数量,那么开始执行清理
while((expired*100/sampled) > config_cycle_acceptable_stale):
//该db没有过期键,不处理
if((num = dictSize(db->expires)) == 0):
break
max_buckets = num*20; //最大的hash桶数量
checked_buckets = 0; //当前检查的hash桶
//采样数量小于每个db的默认检查键数量,当前检查的桶小于最大桶
while(sampled < num && checked_buckets < max_buckets):
//从当前db的过期游标开始检查过期键
idx = db->expires_cursor
idx &= db->expires->ht[table].sizemask
dictEntry *de = db->expires->ht[table].table[idx]
while(de):
dictEntry *e = de
de = de->next
ttl = dictGetSignedIntegerVal(e)-now;
//如果key过期,删除并计数+1
if (activeExpireCycleTryExpire(db,e,now)) expired+=1;
//记录采样数据
if (ttl > 0):
ttl_sum += ttl;//为了统计平均ttl用的
ttl_samples+=1;
samples += 1
db->expires_cursor++;
total_expired += expired
total_sampled += sampled
//超时则退出,并标记
if(ustime()-start > timelimit):
timelimit_exit = 1;
server.stat_expired_time_cap_reached_count++;
break
我们可以总结以下redis定期删除的工作模式:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。