11、Redis 源码解析 - Redis 过期策略

过期策略

先看一下redisDb的结构:

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 */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

第一个字段dict在Redis 源码解析 - Redis 单机数据库一中详细介绍过了,这节看第二个字段expires,如果用户存储KV对时设置了Key的过期时间,就会向expires中添加一组KV,其中value存储的就是该key的过期时间。

过期时间设置

SETEX、PSETEX可以给字符串设置value的时候带上过期时间,PEXPIREAT、EXPIREAT可以单独给某个key设置过期时间,TTL命令查询某个key的过期时间。
与过期时间相关的实现都在expire.c中

// 设置key的过期时间
void setExpire(client *c, redisDb *db, robj *key, long long when) {
   
     
    dictEntry *kde, *de;

    /* Reuse the sds from the main dict in the expire dict */
    kde = dictFind(db->dict,key->ptr);
    serverAssertWithInfo(NULL,key,kde != NULL);
    // 重写key的过期时间
    de = dictAddOrFind(db->expires,dictGetKey(kde));
    dictSetSignedIntegerVal(de,when);
	...
}
// 与set命令有关的过期时间设置
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
   
     
    long long milliseconds = 0; /* initialized to avoid any harmness warning */

    if (expire) {
   
     
        if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
            return;
        if (milliseconds <= 0) {
   
     
            addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
            return;
        }
        if (unit == UNIT_SECONDS) milliseconds *= 1000;
    }
	...
	// 设置过期时间
    if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
    ...
    addReply(c, ok_reply ? ok_reply : shared.ok);
}

// 直接设置key的过期时间
void expireGenericCommand(client *c, long long basetime, int unit) {
   
     
    robj *key = c->argv[1], *param = c->argv[2];
    long long when; /* unix time in milliseconds when the key will expire. */

    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != C_OK)
        return;

    if (unit == UNIT_SECONDS) when *= 1000;
    when += basetime;

    /* No key, return zero. */
    // key不存在或者已经过期
    if (lookupKeyWrite(c->db,key) == NULL) {
   
     
        addReply(c,shared.czero);
        return;
    }

	// 检查要设置的过期时间是否是一个过去的时间
    if (checkAlreadyExpired(when)) {
   
     
    	// 如果是过去时间,直接删除key
        robj *aux;

        int deleted = server.lazyfree_lazy_expire ? dbAsyncDelete(c->db,key) :
                                                    dbSyncDelete(c->db,key);
        serverAssertWithInfo(c,key,deleted);
        server.dirty++;

        /* Replicate/AOF this as an explicit DEL or UNLINK. */
        aux = server.lazyfree_lazy_expire ? shared.unlink : shared.del;
        rewriteClientCommandVector(c,2,aux,key);
        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);
        addReply(c, shared.cone);
        return;
    } else {
   
     
    	// 向expire中插入过期时间
        setExpire(c,c->db,key,when);
        addReply(c,shared.cone);
        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
        server.dirty++;
        return;
    }
}

过期健的清理

当key被设置了过期时间之后,时间到达之后,key是如何被删除的呢,主要在三个时机清理:
1、 用户操作key的时候,一般都会先向DB中查询这个key是否存在,在这个过程中会向expire中查询这个key是否过期,如果过期了,就删除;

robj *lookupKeyWrite(redisDb *db, robj *key) {
   
     
	// 先尝试删除过期健
    expireIfNeeded(db,key);
    return lookupKey(db,key,LOOKUP_NONE);
}

int expireIfNeeded(redisDb *db, robj *key) {
   
     
	// 判断健是否过期
    if (!keyIsExpired(db,key)) return 0;
	
	// slave不主动删除过期key
    if (server.masterhost != NULL) return 1;

    /* Delete the key */
    // 删除key
    server.stat_expiredkeys++;
    propagateExpire(db,key,server.lazyfree_lazy_expire);
    notifyKeyspaceEvent(NOTIFY_EXPIRED,
        "expired",key,db->id);
    return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
                                         dbSyncDelete(db,key);
}

2、 在Redis启动的时候,会向事件循环中注册一个周期性事件serverCron,这个函数包含databasesCron;

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
   
     
	...
	/* Handle background operations on Redis databases. */
    databasesCron();
    ...
}

databasesCron的功能之一就是定期清理过期健:

void databasesCron(void) {
   
     
    /* Expire keys by random sampling. Not required for slaves
     * as master will synthesize DELs for us. */
    if (server.active_expire_enabled) {
   
     
        if (server.masterhost == NULL) {
   
     
        	// 如果是master,则尝试清理过期key
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
        } else {
   
     
            expireSlaveKeys();
        }
    }
    ...
}

3、 Redis时间循环在陷入wait之前,会调用beforeSleep,在这个函数中也会清理过期健;

void beforeSleep(struct aeEventLoop *eventLoop) {
   
     
    UNUSED(eventLoop);
	
	...
    /* Run a fast expire cycle (the called function will return
     * ASAP if a fast cycle is not needed). */
    if (server.active_expire_enabled && server.masterhost == NULL)
        activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);
    ...
}

activeExpireCycle定期清理过期健

为了避免过期健占用大量的内存,activeExpireCycle用来清理过期健,由于Redis的单线程模型,如果过期key很多,扫描一次会很耗时,这样可能会阻塞服务端,所以activeExpireCycle就不可能占用太多时间,所以Redis对于activeExpireCycle的执行时间控制就很精确了,对这个函数的要求是既要执行时间可控,又要尽可能多的清理过期健,减少内存占用。

void activeExpireCycle(int type) {
   
     
	...
	if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
   
     
		// ACTIVE_EXPIRE_CYCLE_FAST类型,并且上次该函数运行没有超时
        /* Don't start a fast cycle if the previous cycle did not exit
         * for time limit. Also don't repeat a fast cycle for the same period
         * as the fast cycle total duration itself. */
        if (!timelimit_exit) return;
        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
        last_fast_cycle = start;
    }
    ...
    // 计算本次运行的时间:
    // 1、ACTIVE_EXPIRE_CYCLE_SLOW类型:在databasesCron中调用,运行时间由ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC控制,该值是一次事件循环框架的运行时间百分比,一次事件循环框架的时间由server.hz控制,为1000000/server.hz ms
    // 2、ACTIVE_EXPIRE_CYCLE_FAST类型:在beforeSleep中调用,运行时间为ACTIVE_EXPIRE_CYCLE_FAST_DURATIONms
    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. */
    // 逐个db开始清理
    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
   
     
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);

        /* Increment the DB now so we are sure if we run out of time
         * in the current DB we'll restart from the next. This allows to
         * distribute the time evenly across DBs. */
        current_db++;
        /* Continue to expire if at the end of the cycle more than 25%
         * of the keys were expired. */
        do {
   
     
        	...
        	/* If there is nothing to expire try next DB ASAP. */
        	// 计算db中需要检查清理的key个数
            if ((num = dictSize(db->expires)) == 0) {
   
     
                db->avg_ttl = 0;
                break;
            }
            slots = dictSlots(db->expires);
            now = mstime();

            /* When there are less than 1% filled slots getting random
             * keys is expensive, so stop here waiting for better times...
             * The dictionary will be resized asap. */
            if (num && slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;
            ...
            while (num--) {
   
     
                dictEntry *de;
                long long ttl;
				// 选择一个随机key
                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                ttl = dictGetSignedIntegerVal(de)-now;
                // 判断key是否过期,如果过期,则删除
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                ...
                total_sampled++;
            }
            ...
        }while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    ...
}

activeExpireCycle的运行中存在诸多条件的限制,主要还是因为Redis单线程、内存服务的模型权衡。