在redis数据库中,可以对键值对设置过期时间。当键值对过期时,redis会通过一定的机制将过期键删除。
redis的过期键删除策略有两种:定期删除和惰性删除。
惰性删除
惰性删除是每次获取键值对时,都对获取的键进行过期检查,如果过期的话,就删除该键值对;如果没过期,就返回该键。
惰性删除策略的好处是对cpu时间比较友好,每次只检查当前处理的键值对,不会对其他过期的键值对花费
cpu时间。
惰性删除策略的缺点就是对内存不友好,一个键已经过期,但是这个键还会留在数据库中,只要它不被访问,就不会被删除,一直占用这内存空间。如果这些过期键长期不被访问,将永远不会被删除,我们可以视作内存泄漏——无用的数据占据了大量内存,而服务器不去释放它。当然,用户可以通过手动执行FLUSHDB来删除过键。
过期键的惰性删除策略由db.c/expireifNeeded函数实现,所有对数据库的读写命令执行之前都会调用
expireifNeeded来检查命令执行的键是否过期:
1)键过期,expireifNeeded将键从数据库中删除
2)键未过期,expireifNeeded不作任何处理
int expireIfNeeded(redisDb *db, robj *key) {
mstime_t when = getExpire(db,key);
mstime_t now;
if (when < 0) return 0; /* No expire for this key */
now = server.lua_caller ? server.lua_time_start : mstime();
if (server.masterhost != NULL) return now > when;
/* Return when this key has not expired */
if (now <= when) return 0;
// 删除过期键
server.stat_expiredkeys++;
propagateExpire(db,key,server.lazyfree_lazy_expire);
//server.lazyfree_lazy_expire为1进行异步删除(懒空间释放),反之同步删除
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
//获取键的过期时间
long long getExpire(redisDb *db, robj *key) {
dictEntry *de;
if (dictSize(db->expires) == 0 ||
(de = dictFind(db->expires,key->ptr)) == NULL) return -1;
return dictGetSignedIntegerVal(de);
}
定期删除
定期删除是每隔一段时间,程序对数据库检查一次,删除数据库里的过期键。至于每次检查多少数据库,删除多少过期键,由算法决定。
定期删除策略的关键点就是删除操作执行的时长和频率:
1)如果删除操作太过频繁或者执行时间太长,就对cpu时间不是很友好,cpu时间过多的消耗在删除过期键上。
2)如果删除操作执行太少或者执行时间太短,就不能及时删除过期键,导致内存浪费。
定期删除策略由expire.c/activeExpireCycle函数实现。在redis事件驱动的循环中的eventLoop->beforesleep和周期性操作databasesCron都会调用activeExpireCycle来处理过期键。activeExpireCycle在规定的时间,分多次遍历各个数据库,从expires字典中随机检查一部分过期键的过期时间,删除其中的过期键。
//周期性操作中进行慢速过期键删除, 执行频率同databasesCron的执行频率
//执行时长为1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100
void databasesCron(void) {
if (server.active_expire_enabled && server.masterhost == NULL) {
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
} else if (server.masterhost != NULL) {
expireSlaveKeys();
}
}//进行快速过期键删除,执行间隔和执行时长都为ACTIVE_EXPIRE_CYCLE_FAST_DURATION
void beforeSleep(struct aeEventLoop *eventLoop) {
if (server.active_expire_enabled && server.masterhost == NULL)
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);
}
activeExpireCycle函数实现
void activeExpireCycle(int type) {
static unsigned int current_db = 0; /* 上次定期删除遍历到的数据库id*/
static int timelimit_exit = 0; /* Time limit hit in previous call? */
static long long last_fast_cycle = 0; /* 上一次执行快速定期删除的时间点 */
int dbs_per_call = CRON_DBS_PER_CALL;//每次定期删除,遍历的数据库的数量
long long start = ustime(), timelimit;
if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
if (!timelimit_exit) return;
//快速定期删除的时间间隔是ACTIVE_EXPIRE_CYCLE_FAST_DURATION
//ACTIVE_EXPIRE_CYCLE_FAST_DURATION是快速定期删除的执行时长
if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
last_fast_cycle = start;
}
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
//慢速定期删除的执行时长
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; /*删除操作的执行时长 */
for (j = 0; j < dbs_per_call; 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)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
//从db->expires中随机选取num个键进行检查
while (num--) {
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
ttl = dictGetSignedIntegerVal(de)-now;
//过期检查,并对过期键进行删除
if (activeExpireCycleTryExpire(db,de,now)) expired++;
……
}
……
//每次检查只删除ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4个过期键
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
}