Redis key过期删除机制实现分析

文章目录

前言

当我们创建Redis key时,可以通过expire命令指定key的过期时间(TTL),当超过指定的TTL时间后,key将会失效。

那么当key失效后,Redis会立刻将其删除么?如果不会,那么何时Redis才将其真正的删除呢?我们来一起一探究竟。

Redis key过期淘汰机制

Redis中的key过期淘汰机制是由两种方式实现:

  • 惰性删除机制
  • 定时扫描删除机制

两种模式都不会在key达到过期时间后,第一时间删除key,而是等待特定的时机触发淘汰机制,这个很好理解,如果每一个key到达过期时间后,redis都需要第一时间检测到,并将其删除,那么将会消耗大量的资源,去实时的扫描全部key值,这显然是不合理的。

下面我们来看一下两种方式的具体实现机制。

惰性删除机制

惰性删除很简单,就是当有客户端的请求查询该 key 的时候,检查下 key 是否过期,如果过期,则删除该 key。

在此种模式下,触发key淘汰的时机,是将删除过期数据的主动权交给了每次访问请求。

那么Redis具体是如何实现的,我们来一起看一下源码实现。

淘汰删除的具体实现,在db.c#expireIfNeeded()

c 复制代码
int expireIfNeeded(redisDb *db, robj *key) {
    /* 通过调用getExpire函数获取key的过期时间。*/
    mstime_t when = getExpire(db,key);
    mstime_t now;
	
    /* 当过期时间小于0时,表示key没有设置过期时间,直接返回0 */
    if (when < 0) return 0; /* No expire for this key */

    /* 如果Redis正在进行数据加载,直接返回0,不进行后续的过期检查。 */
    if (server.loading) return 0;

    /* 获取当前时间,如果当前是在执行Lua脚本中,使用server.lua_time_start作为当前时间;否则,使用系统当前时间mstime作为当前时间 */
    now = server.lua_caller ? server.lua_time_start : mstime();

    /* 如果Redis是主从复制模式,并且当前节点是从节点,则直接返回当前时间是否大于过期时间,不进行后续的过期操作 */
    if (server.masterhost != NULL) return now > when;

    /* 如果当前时间小于等于过期时间,则直接返回0,表示key还没有过期 */
    if (now <= when) return 0;

    /* Delete the key */
    /* 增加已过期key的数量统计 */
    server.stat_expiredkeys++;
    /* 向从节点发送key过期的命令,保证从节点也能及时删除过期的key */
    propagateExpire(db,key);
    /* 向Redis的事件通知机制发送key过期的事件通知 */
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);
    /* 删除已过期的key,并返回1表示删除成功 */
    return dbDelete(db,key);
}

/* Delete a key, value, and associated expiration entry if any, from the DB */
int dbDelete(redisDb *db, robj *key) {
    /* Deleting an entry from the expires dict will not free the sds of
     * the key, because it is shared with the main dictionary. */
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
    if (dictDelete(db->dict,key->ptr) == DICT_OK) {
        return 1;
    } else {
        return 0;
    }
}

上面的源码即Redis执行key淘汰删除的核心过程,具体操作可以参见注释,通过方法名字expireIfNeeded()这是一个检查类型的方法,那么说明是在进行key操作时,会触发该方法进行检查key是否需要进行淘汰删除,那么其调用时机在何时呢?

db.c#lookupKeyRead()与lookupKeyWrite()

c 复制代码
robj *lookupKeyRead(redisDb *db, robj *key) {
    robj *val;

    expireIfNeeded(db,key);
    val = lookupKey(db,key);
    if (val == NULL)
        server.stat_keyspace_misses++;
    else
        server.stat_keyspace_hits++;
    return val;
}

robj *lookupKeyWrite(redisDb *db, robj *key) {
    expireIfNeeded(db,key);
    return lookupKey(db,key);
}

robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply) {
    robj *o = lookupKeyRead(c->db, key);
    if (!o) addReply(c,reply);
    return o;
}

robj *lookupKeyWriteOrReply(redisClient *c, robj *key, robj *reply) {
    robj *o = lookupKeyWrite(c->db, key);
    if (!o) addReply(c,reply);
    return o;
}

上面的代码是调用expireIfNeeded()的上游function,通过名字可以看出,#lookupKeyRead()与lookupKeyWrite()是读取和写入key的方法(不得不说,redis的代码命名非常的优秀,值得我们学习),那么调用该方法的一定就是执行获取key的地方,这里我们以最简单的stringget命令为例:

t_string.c#getCommand()

c 复制代码
/* string的get命令 */
void getCommand(redisClient *c) {
    getGenericCommand(c);
}

int getGenericCommand(redisClient *c) {
    robj *o;
	/* 通过调用lookupKeyReadOrReply函数查找指定key的值,如果key不存在,则向客户端返回空值并返回REDIS_OK;如果查找到了key的值,则将值保存到变量o中,继续后续的操作 */
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
        return REDIS_OK;
	
    /* 判断获取到的值的类型是否为字符串类型 */
    /* 如果值的类型不是字符串类型,向客户端返回错误响应,并返回REDIS_ERR表示获取失败 */
    if (o->type != REDIS_STRING) {
        addReply(c,shared.wrongtypeerr);
        return REDIS_ERR;
    } else {
        /* 如果值的类型是字符串类型,向客户端返回获取到的字符串值,并返回REDIS_OK表示获取成功 */
        addReplyBulk(c,o);
        return REDIS_OK;
    }
}

上述就是string get命令的执行过程,我们可以清晰的看到,redis是如何实现惰性淘汰删除机制,其他的数据结构,例如HashListSetZset也是如此,这里就不一样贴出源码进行举例说明了,感兴趣的读者可以翻阅redis源码。

这里我们用一张string get命令的时序图,总结一下get命令的执行流程:

定时扫描删除机制

上面部分我们了解了惰性淘汰删除机制,但是仅仅靠客户端访问来判断 key 是否过期才执行删除肯定不够,因为有的 key 过期了,但未来再也没人访问,那岂不是GG,这些数据要怎么删除呢?

Redis在后台,会启动一个定时任务,定期扫描数据库中的所有key,检查它们的过期时间是否已到期。但是这里需要注意,定时任务并不是一次运行就检查所有的库,所有的键,而是随机检查一定数量的键。

为什么是随机抽查,而不是全量从头到尾扫描一遍?

很好理解,如果redis中的key特别多,如果进行全量扫描,那对redis的性能会存在巨大的影响,如果有一个亿的key,每次定时任务执行都进行全量扫描,CPU岂不是爆炸。

上图的流程图,简单的描述了定时任务的执行逻辑(实际上会复杂很多),还是老规矩,不多逼逼,上源码,Redis具体是如何实现的,我们来一起看一下源码实现。

定时任务的实现在redis.c#activeExpireCycle()

c 复制代码
void activeExpireCycle(int type) {
    ....此处省略部分前置逻辑
	
    /* 循环redis全部的db */
    for (j = 0; j < dbs_per_call; j++) {
        ....此处省略部分前置逻辑

        do {
            unsigned long num, slots;
            long long now, ttl_sum;
            int ttl_samples;
			
            /* 获取dict中设置了TTL的key集合中,计算集合的数量 */
            if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
                break;
            }
            
            ....此处省略部分前置逻辑
			
            /* 如果过期的key数量,超过了20,那么扫描数量设置为20 */
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; /* ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP = 20 */
			
            /* 循环处理 */
            while (num--) {
                dictEntry *de;
                long long ttl;
				
                /* 获取dict中设置了TTL的key集合中,随机获取一个key */
                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                /* 计算TTL剩余时间 */
                ttl = dictGetSignedIntegerVal(de)-now;
                /* 如果当前的key已经过期,则执行删除操作,并将过期key的数量加1 */
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                
                if (ttl < 0) ttl = 0;
                ttl_sum += ttl;
                ttl_samples++;
            }

            ....此处省略部分后置逻辑
        /* 如果过期的key数量,超过阈值的25%,继续循环,否则退出扫描 */
        /* 这也就意味着在任何时候,过期 key 的最大数量等于每秒最大写入操作量除以4 = 5*/        
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
}

上面的代码就是定时任务扫描过期key的执行流程,笔者删除了部分代码,仅保留的核心执行部分,方便读者阅读,核心执行逻辑可以参见注释部分

如果用一句话概括说明定时任务的流程,那么可以总结为:

定时任务循环扫描每个redis数据库,从设置了TTL的key的集合中,随机挑选N个key进行检查,如果过期,干掉,否则跳过,直到过期key的数量小于25%,退出扫描

以上,就是Redis删除过期key的两种实现方式,由于笔者对C的理解很有限,因此仅仅截取了部分源码进行解读,也可能有很多解读不对的地方,望读者见谅。

事实上,仅仅通过惰性删除+定时任务扫描,仍会可能存在很多"漏网之鱼",毕竟定时任务删除,并非全量扫描,那么如果Redis的使用容量达到了最大内存,Redis会如何操作?

这就涉及到了Redis的key淘汰策略,本篇的内容就此为止,关于Redis的淘汰策略解读,我们下次再聊。

相关推荐
IT大白鼠8 小时前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz8 小时前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙10 小时前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除11 小时前
redis--018
redis
两锭子12 小时前
Redis基础
redis
烟沙九洲12 小时前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索12 小时前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞13 小时前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路1 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog1 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis