Redis源码分析:过期key删除与设置key的过期时间

Redis中设置key过期时间与过期key的处理流程

在Redis中,可以再设置值的时候就设置该Key的过期时间,也可以通过在expire命令来设置某个key值的过期时间,并且在了解完设置过期时间之后,来查看一下Redis的过期key的处理流程。本文先来了解一下Redis的过期键的相关流程。

Redis设置key的过期时间

通过set设置过期时间
/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
void setCommand(client *c) {
    int j;
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = OBJ_SET_NO_FLAGS;

    for (j = 3; j < c->argc; j++) {                                     // 判断set过程中输入的几个参数
        char *a = c->argv[j]->ptr;
        robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];            // 获取下一个输入参数

        if ((a[0] == 'n' || a[0] == 'N') &&
            (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
            !(flags & OBJ_SET_XX))
        {
            flags |= OBJ_SET_NX;                                        // 判断是否是NX标志,只有在键不存在的情况下操作
        } else if ((a[0] == 'x' || a[0] == 'X') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_NX))
        {
            flags |= OBJ_SET_XX;                                        // 判断是否是XX标志,只有在键存在的情况下才操作
        } else if ((a[0] == 'e' || a[0] == 'E') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_PX) && next)
        {
            flags |= OBJ_SET_EX;                                        // 是否设置的过期时间为秒
            unit = UNIT_SECONDS;
            expire = next;                                                      
            j++;
        } else if ((a[0] == 'p' || a[0] == 'P') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_EX) && next)
        {
            flags |= OBJ_SET_PX;                                        // 是否设置的过期时间为毫秒
            unit = UNIT_MILLISECONDS;
            expire = next;
            j++;
        } else {
            addReply(c,shared.syntaxerr);                               // 参数解析失败则返回语法错误
            return;
        }
    }

    c->argv[2] = tryObjectEncoding(c->argv[2]);                         // 对传入值进行编码
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);   // 调用设置命令
}

从整个流程来看,set在检查完输入的相关标志之后,就直接调用了setGenericCommand方法来进行设置,该方法就是最终对key进行过期设置的函数,

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) {                                                    // 如果转换成微秒时间小于0则传入的时间有误
            addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
            return;
        }
        if (unit == UNIT_SECONDS) milliseconds *= 1000;                             // 转换成毫秒
    }

    if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||        
        (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))                  // 检查是否需要建存在的情况 如果是NX表示键不存在则操作 如果XX键存在才能操作
    {
        addReply(c, abort_reply ? abort_reply : shared.nullbulk);                   // 如果不满足条件则返回中止命令
        return;
    }
    setKey(c->db,key,val);                                                          // 往对应的数据库中设置该key
    server.dirty++;
    if (expire) setExpire(c,c->db,key,mstime()+milliseconds);                       // 如果有过期时间,则调用setExpire来进行过期时间的设置
    notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);                         // 通知事件
    if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC, 
        "expire",key,c->db->id); 
    addReply(c, ok_reply ? ok_reply : shared.ok);                                   // 返回设置成功
}

通过流程的概述可知,在最后需要调用setExpire来真正的将对应的过期时间设置进去。

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);                          // 从db中查找到对应的key的entry
    serverAssertWithInfo(NULL,key,kde != NULL);                 // 判断是否未找到
    de = dictAddOrFind(db->expires,dictGetKey(kde));            // 在expires中查找对应的entry的key值
    dictSetSignedIntegerVal(de,when);                           // 设置过期时间

    int writable_slave = server.masterhost && server.repl_slave_ro == 0;
    if (c && writable_slave && !(c->flags & CLIENT_MASTER))   
        rememberSlaveKeyWithExpire(db,key);
}

主要通过dictSetSignedIntegerVal来设置到已经查找到的dictEntry中的v对应的s64字段,从而保存查找的时间,至此我们知道了设置的过期的key会被添加到expires这个字典中单独去保存。

通过expire来设置过期时间
/* EXPIRE key seconds */
void expireCommand(client *c) {
    expireGenericCommand(c,mstime(),UNIT_SECONDS);  // 通过秒来过期
}
/* This is the generic command implementation for EXPIRE, PEXPIRE, EXPIREAT
 * and PEXPIREAT. Because the commad second argument may be relative or absolute
 * the "basetime" argument is used to signal what the base time is (either 0
 * for *AT variants of the command, or the current time for relative expires).
 *
 * unit is either UNIT_SECONDS or UNIT_MILLISECONDS, and is only used for
 * the argv[2] parameter. The basetime is always specified in milliseconds. */
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. */
    if (lookupKeyWrite(c->db,key) == NULL) {                                        // 先查找该Key 如果没有查找到则返回空
        addReply(c,shared.czero);
        return;
    }

    /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past
     * should never be executed as a DEL when load the AOF or in the context
     * of a slave instance.
     *
     * Instead we take the other branch of the IF statement setting an expire
     * (possibly in the past) and wait for an explicit DEL from the master. */
    if (when <= mstime() && !server.loading && !server.masterhost) {                // 如果过期的时间小于当前获取的时间 如果server不是loading状态并且不是Master模式
        robj *aux;

        int deleted = server.lazyfree_lazy_expire ? dbAsyncDelete(c->db,key) :
                                                    dbSyncDelete(c->db,key);        // 是否是惰性删除
        serverAssertWithInfo(c,key,deleted);                                        // 检查该key是否删除
        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 {
        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;
    }
}

通过该epxire命令的执行可知,如果该键还未过期则调用setExpire来设置该键的过期时间,该函数的执行流程已经分析过。

过期key删除
通过get等命令获取key时

检查键的是否过程主要是通过在获取到key时,然后获取key时,来判断该key是否已经过期。在getCommand中,会调用到如下流程

void getCommand(client *c) {
    getGenericCommand(c);
}
int getGenericCommand(client *c) {
    robj *o;

    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)  // 先查找该key
        return C_OK;

    if (o->type != OBJ_STRING) {
        addReply(c,shared.wrongtypeerr);
        return C_ERR;
    } else {
        addReplyBulk(c,o);
        return C_OK;
    }
}
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) {
    robj *o = lookupKeyRead(c->db, key);    // 查找该key
    if (!o) addReply(c,reply);
    return o;
}
robj *lookupKeyRead(redisDb *db, robj *key) {
    return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
}

在lookupKeyReadWithFlags函数中,有该expireIfNeeded来判断是否过期删除;

int expireIfNeeded(redisDb *db, robj *key) {
    if (!keyIsExpired(db,key)) return 0;                                        // 检查是否是过期了 如果没有过期则返回0

    /* If we are running in the context of a slave, instead of
     * evicting the expired key from the database, we return ASAP:
     * the slave key expiration is controlled by the master that will
     * send us synthesized DEL operations for expired keys.
     *
     * Still we try to return the right information to the caller,
     * that is, 0 if we think the key should be still valid, 1 if
     * we think the key is expired at this time. */
    if (server.masterhost != NULL) return 1;

    /* Delete the 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);              // 判断是通过惰性删除还是同步删除
}

这就是在获取的过程中来删除已经过期的key,删除的过程中既要在数据库中保存的数据删除还需要在过期字典中删除。

定时任务删除过期key

从以上流程可知,设置key的过期都是设置了过期时间之后就执行完成,此时,还需要知道什么时候会去检查哪些key过期了呢,什么时候去检查呢,答案就是在Redis服务器启动的时候,初始化了一个serverCron函数,定时执行;在该函数中有如下过程;

databasesCron  -> activeExpireCycle

activeExpireCycle函数就是删除对应过期key的执行函数;

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;
    long long start = ustime(), timelimit, elapsed;

    /* When clients are paused the dataset should be static not just from the
     * POV of clients not being able to write, but also from the POV of
     * expires and evictions of keys not being performed. */
    if (clientsArePaused()) return;

    if (type == 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;                                                    // 开始时间设置成最后一次时间
    }

    /* We usually should test CRON_DBS_PER_CALL per iteration, with
     * two exceptions:
     *
     * 1) Don't test more DBs than we have.
     * 2) If last time we hit the time limit, we want to scan all DBs
     * in this iteration, as there is work to do in some DB and we don't want
     * expired keys to use memory for too much time. */
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;                                                // 限制访问的数据库数量

    /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time
     * per iteration. Since this function gets called with a frequency of
     * server.hz times per second, the following is the max amount of
     * microseconds we can spend in this function. */
    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. */       // 转换成微秒

    /* Accumulate some global stats as we expire keys, to have some idea
     * about the number of keys that are already logically expired, but still
     * existing inside the database. */
    long total_sampled = 0;
    long total_expired = 0;

    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 {
            unsigned long num, slots;
            long long now, ttl_sum;
            int ttl_samples;
            iteration++;

            /* If there is nothing to expire try next DB ASAP. */
            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;

            /* The main collection cycle. Sample random keys among keys
             * with an expire set, checking for expired ones. */
            expired = 0;
            ttl_sum = 0;
            ttl_samples = 0;

            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;             // 获取过期key的次数

            while (num--) {
                dictEntry *de;
                long long ttl;

                if ((de = dictGetRandomKey(db->expires)) == NULL) break;        // 随机获取key
                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;                                           // 加上已经删除的key的数量

            /* Update the average TTL stats for this database. */
            if (ttl_samples) {
                long long avg_ttl = ttl_sum/ttl_samples;

                /* Do a simple running average with a few samples.
                 * We just use the current estimate with a weight of 2%
                 * and the previous estimate with a weight of 98%. */
                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);               // 更新平均过期的比率
            }

            /* We can't block forever here even if there are many keys to
             * expire. So after a given amount of milliseconds return to the
             * caller waiting for the other active expire cycle. */
            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
                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%则停止
    }

    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);
}

该函数主要分两种情况下的删除;

  1. 快速删除,即该函数将尝试不超过一定时间内不重复执行的策略,即不超过两个1000微秒
  2. 慢删除,即通过时间限制为REDIS_HZ周期的百分比,来执行删除的过期key

通过随机的抽取百分之二十五的key来达到删除过期key的目的,从而保证了一些过期的key在很久不访问时占用内存资源。

总结

本文主要是大致了解了一下,在Redis中key的过期设置的主要的流程,Redis是如何通过访问key时检测与定时任务来清理过期的key从而节省空间。由于本人才疏学浅,如有错误请批评指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章