前言
本文主要全面分析redis如何来设置过期key,跟过期相关的操作,哪些我们认为可能会有影响但实际不会产生影响的操作,以及过期键的处理策略。本文会从源码层面聊到一些其他文章不曾有的部分
废话不多说,下面正式进入主题
一、键过期的相关命令
1.对过期有影响的操作
有键覆盖操作的命令如:DEL,SET,GETSE,RENAME, 这里重点说一下set和rename操作,
set操作可以指定过期时间,无论以前键是否带有过期时间,都以新的时间为准,如果不设置那会从expire 字典里面移除掉,即永不过期。
rename 效果上差不多无论,被覆盖的旧key 是何种属性,都已新的为准
如图:
2.对过期无影响的操作
对于那些仅仅对value 进行修改的值本质上不对key的属性做修改的,一些命令是不会对ttl 有任何影响的。如incr ,LPUSH,RPUSH,HSET … 等等
3.过期命令,和拯救正在过期键的命令
过期命令我们常用到的就是expire和pexpire, 两个命令的区别一个是秒为单位,另位一个是已毫秒为单位
expire 的参数是一个整数代表键会在几秒后会过期。可通过ttl去查看他的过期时间
如果传递的是一个负数会立刻将键值删除
persist 是除了set rename 这些覆盖操作另外一个可以拯救正在过期key的命令,其余两个会跟具体的值挂钩。
但是要注意persist 已经过期的键的时候会立即触发删除,也就是说persist 并不能拯救已经删除的key
另外persist 虽然可以在子实例执行成功,即使是一个过期的键,但子实例仍会被主实例相关的同步删除给删除掉。
4.过期命令相关源码分析
redis.h
/* Redis database representation. There are multiple databases identified
* by integers from 0 (the default database) up to the max configured
* database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
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 */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
首先注意到的所有过期键是放在一个map里面,这里面跟lru不同的是,lru会通过字段存在键对象里面,为什么了个人觉得有以下两个观点。
- 从目的分析下这两者的不同,过期集合的设置是为了再内存未满之前腾出更多的空间,而lru是内存满了之后所要执行的策略。
- 接着上面的目的,设置集合我们采取删除策略的时候才会更有效率,当然这里需要结合后面讲的过期删除策略有关系
expire.c
/* EXPIRE key seconds */
/**
* 参数为秒的时候要获取系统的现在时间
* mstime()这个方法
* @param c
*/
void expireCommand(client *c) {
expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
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. */
// 取出 when 参数 主要是验证传入的参数是否合法,比如是否传入是字符
if (getLongLongFromObjectOrReply(c, param, &when, NULL) != C_OK)
return;
//如果是秒为单位,则when 需要*1000
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)) {
robj *aux;
//过期的话就删除key,这里有判断是否开启异步删除
int deleted = server.lazyfree_lazy_expire ? dbAsyncDelete(c->db,key) :
dbSyncDelete(c->db,key);
serverAssertWithInfo(c,key,deleted);
//键的改动都会加1
server.dirty++;
/* Replicate/AOF this as an explicit DEL or UNLINK. */
aux = server.lazyfree_lazy_expire ? shared.unlink : shared.del;
//修改客户端参数,但是这里的客户端指的是其内部处理,会和同步产生关系。
//跟写入aof的日志挂钩
rewriteClientCommandVector(c,2,aux,key);
//每次键改动都会调用该方法,旧版本主要为了通知被watched 的键,而新版本还会去追踪错误的信息
signalModifiedKey(c,c->db,key);
//这一部分的代码主要跟内存回收有关的事件通知。
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);
addReply(c, shared.cone);
return;
} else {
//如果时间合法未过期,则设置到expire 字典里面去, key 为传入的key, when 为计算好的时间戳
setExpire(c,c->db,key,when);
//设置返回
addReply(c,shared.cone);
//更上面注释一样,通知到钩子
signalModifiedKey(c,c->db,key);
//这一部分的代码主要跟内存回收有关的事件通知。
notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
server.dirty++;
return;
}
}
大体的流程为 判断参数是否合法->判断键是否存在键值空间->判断传入时间是否超时->超时改为删除操作,并对操作记录进行更改,然后一些通知事件->不超时,将键值设置到过期字典里面去。
由于以上注释已经非常详细,可以多看上面的注释
下一篇文章将展开过期策略是如何运行的,敬请留意
相关参考:
redis 6.0.7 的源码 https://github.com/redis/redis
redis 3.0 带注释的的源码 https://github.com/huangz1990/redis-3.0-annotated