redis源码之ttl(三)

简介

redis可以设置key的ttl(time to live, 存活时长),当时间超过ttl了,就会删除key。通过源码阅读了解下面两个问题:

  • redis如何保存ttl信息
  • redis什么时候触发删除过期key的操作

源码

ttl的保存

在set key的时候设置或者更新ttl

#t_string.c  set命令的功能函数
# 参数expire是过期时间,类型是robj,但是可以从中读取到long long类型的值,表示的是相对当前时间
# unit是时间单位,表示是毫秒或者秒为单位,如果是以秒为单位,就要乘以1000转为毫秒为单位
# 所以redis的ttl的精度是毫秒级别的
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 ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
        (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
    {
        addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
        return;
    }
    genericSetKey(c,c->db,key,val,flags & OBJ_SET_KEEPTTL,1);
    server.dirty++;
	   // redis记录的是绝对时间,所以这里加上了当前时间mstime()
    if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
    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);
}

// db.c 设置key的过期时间
// 将key放入expires字典中,dictEntry的v字段记录了键值过期的绝对时间
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);
	   // 在字典db->expires中查找kde->key(如果不存在key,就为key创建dictEntry)并
	   // 返回对应的dictEntry *
    de = dictAddOrFind(db->expires,dictGetKey(kde));
	   // de->v.s64 = when 将when放入de, 所以在expire字典中,每个dictEntry存放了key的过期时间
	   // 如果没有设置ttl,就不会把key放入expires字典中
    dictSetSignedIntegerVal(de,when);

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

// dict.h
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
		     // 在expires字典中, dictEntry的v.s64记录了key的绝对过期时间,单位是毫秒
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

删除过期key

过期key的删除可以分为被动删除和主动删除:

  • 被动删除是访问到key的时候发现key过期了,就删除
  • 主动删除是通过定时任务周期性的遍历检查expires字典中是否存在过期的key。

被动删除源码:

//db.c  下面是删除过期key的功能函数
int expireIfNeeded(redisDb *db, robj *key) {
    // 如果key没有过期
    if (!keyIsExpired(db,key)) return 0;

    // key过期了
    /* 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. */
	   // 如果是备机上执行这个函数, 并不会在备机上删除key
	   // 备机删除是等主机发送DEL命令过来触发删除操作
    if (server.masterhost != NULL) return 1;

    /* Delete the key */
	   // 计数
    server.stat_expiredkeys++;
	   // 通知从节点和AOF文件删除key
    propagateExpire(db,key,server.lazyfree_lazy_expire);
    notifyKeyspaceEvent(NOTIFY_EXPIRED,
        "expired",key,db->id);
    // 如果服务器配置了lazyfree_lazy_expire, 那就异步删除,否则同步删除
    int retval = server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
                                             dbSyncDelete(db,key);
    // 通知watch key的客户端key已经更改,会导致exec命令失败
	   // 通知缓存了key的客户端,缓存失效
    if (retval) signalModifiedKey(NULL,db,key);
    return retval;
}
// 只要访问key,都会判断key是否过期,如果过期了就删除key
// 比如插入key之前,检查发现key已经存在且过期,就删除过期的key
// 下面是会调用expireIfNeeded的函数

// get命令调用
lookupKeyReadWithFlags()  db.c
// set命令调用
lookupKeyWriteWithFlags() db.c
// 随机获取key调用
dbRandomKey
// scan命令调用
scanGenericCommand

主动删除过期的key

// 函数名  文件名
// 定时任务,每秒执行10次
serverCron server.c
   databasesCron server.c
    activeExpireCycle expire.c
	     activeExpireCycleTryExpire  expire.c
		      // 获取key的过期时间
		      dictGetSignedIntegerVal
			     // 删除key
			     deleteExpiredKeyAndPropagate
				      // 异步删除 
					    dbAsyncDelete
						   // 同步删除						 
						   

beforeSleep  server.c
  activeExpireCycle expire.c
  

expire字典的检查和回收key分为慢速和快速两种方式:

  • 慢速是通过定时任务,系统每0.1秒会执行一次serverCron, 所以也就是每0.1秒会检查一次expire字典,然后清理过期的key,花更多的时间,更少的频率。
  • 快速每个event loop cycle都会执行一次快速的expire字典检查清理,花更少的时间,更高的频率。
  • 这个工作是累积的,均匀的处理每个数据库,并且每个数据库都会记录上次处理到哪里了,下次接着上次的进行处理。

查看expire统计数据

192.168.1.239:6379> info stats
# Stats
...
expired_keys:0
expired_stale_perc:0.00
expired_time_cap_reached_count:0
expire_cycle_cpu_milliseconds:43
evicted_keys:0
...

# 查看相关配置
192.168.1.239:6379> config get *expire*
1) "lazyfree-lazy-expire"
2) "no"
3) "active-expire-effort"
4) "1"

posted @ 2024-09-27 16:57  董少奇  阅读(82)  评论(0)    收藏  举报