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);                                                          // 往对应的数据库中设置该keyserver.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的entryserverAssertWithInfo(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)  // 先查找该keyreturn 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);    // 查找该keyif (!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;        // 随机获取keyttl = 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;} elsecurrent_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从而节省空间。由于本人才疏学浅,如有错误请批评指正。

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

  1. Redis源码分析:基础概念介绍与启动概述

    Redis源码分析 基于Redis-5.0.4版本,进行基础的源码分析,主要就是分析一些平常使用过程中的内容.仅作为相关内容的学习记录,有关Redis源码学习阅读比较广泛的便是<Redis设计与 ...

  2. Redis源码分析(一)redis.c //redis-server.c

    Redis源码分析(一)redis.c //redis-server.c 入口函数 int main() 4450 int main(int argc, char **argv) {4451 init ...

  3. redis源码分析 -- cs结构之服务器

    服务器与客户端是如何交互的 redis客户端向服务器发送命令请求,服务器接收到客户端发送的命令请求之后,读取解析命令,并执行命令,同时将命令执行结果返回给客户端. 客户端与服务器交互的代码流程如下图所 ...

  4. 10年大厂程序员是如何高效学习使用redis的丨redis源码分析丨redis存储原理

    10年大厂程序员是怎么学习使用redis的 1. redis存储原理分析 2. redis源码学习分享 3. redis跳表和B+树详细对比分析 视频讲解如下,点击观看: 10年大厂程序员是如何高效学 ...

  5. Redis源码分析 —— 发布与订阅

    前言 通过阅读Redis源码,配合GDB和抓包等调试手段,分析Redis发布订阅的实现原理,思考相关问题. 源码版本:Redis 6.0.10 思考问题 发布订阅基本概念介绍 订阅频道 -- SUBS ...

  6. Redis 源码分析之故障转移

    在 Redis cluster 中故障转移是个很重要的功能,下面就从故障发现到故障转移整个流程做一下详细分析. 故障检测 PFAIL 标记 集群中每个节点都会定期向其他节点发送 PING 消息,以此来 ...

  7. Redis源码分析(一)--Redis结构解析

    从今天起,本人将会展开对Redis源码的学习,Redis的代码规模比较小,非常适合学习,是一份非常不错的学习资料,数了一下大概100个文件左右的样子,用的是C语言写的.希望最终能把他啃完吧,C语言好久 ...

  8. Redis源码分析之PSYNC同步

    Redis master-slave 同步源码分析 (1)slave 流程分析 (2)master 流程分析 Slave 分析 当Redis 启动后,会每隔 1s 调用 replicationCron ...

  9. Redis源码分析(十一)--- memtest内存检测

    今天我们继续redis源码test测试包下的其他文件,今天看完的是memtest文件,翻译器起来,就是memory test 内存检测的意思,这个文件虽然说代码量不是很多,但是里面的提及了很多东西,也 ...

  10. Redis源码分析之工具类util

    在redis源码中的辅助工具类中,主要包括大小端转换.SHA算法以及util.h中对应的算法. 大小端转换: LittleEndian:低位字节数据存放于低地址,高位字节数据存放于高地址. BigEn ...

最新文章

  1. layui 常用的css,使用layui xtree插件最基础样式
  2. python 同时给多个变量赋值
  3. Win32汇编获取和设置文本框的内容
  4. matlab api接口调用json格式
  5. 国内视频云市场转入整合阶段
  6. 循环判定闰年的程序_C语言入门教程(六)for循环
  7. 众多SEO专家集体盛赞
  8. [maven] springboot将jar包打包到指定目录
  9. C++ STL string的输出
  10. [Ajax] jQuery中的Ajax -- 01-jQuery中的Ajax
  11. AI学习笔记(十八)NLP常见场景之情感分析
  12. PostgreSQl中 index scan 代价的进一步学习
  13. c语言必背100代码,C语言代码大全(c语言必背项目代码)
  14. MATLAB App Designer 制作一个简易计算器的课堂作业
  15. natapp做一个内网穿透
  16. 网络速率与TCP窗口大小的关系
  17. Leftist Heaps
  18. 正态分布的概率密度函数 python_多元正态分布概率密度函数的三维绘图
  19. python基础教程四级查数据_四六级成绩还可以这样查?Python助你装B一步到位!!!...
  20. Android 蓝牙 搜索周围设备代码流程分析-framework到协议栈流程

热门文章

  1. 2个月做出一款AI项目?这些学生在DeeCamp上决出两个总冠军
  2. 漫谈 ClickHouse 在实时分析系统中的定位与作用
  3. 知乎多场景内容匹配方案荣获CSDN AI优秀案例奖
  4. 全球Python调查报告:Python 2正在消亡,PyCharm比VS Code更受欢迎
  5. 深度 | 一文读懂“情感计算”在零售中的应用发展
  6. 百万人学AI:CSDN重磅共建人工智能技术新生态
  7. 万人马拉松赛事,人脸识别系统如何快速、准确完成校验?
  8. 从多媒体技术演进看AI技术
  9. 对话腾讯AI Lab:即将开源自动化模型压缩框架PocketFlow,加速效果可达50%
  10. Google把AI芯片装进IoT设备,与国内造芯势力何干?