redis4.0的时间事件与expire

简介

时间事件和文件事件有着相似的接口,他们都在aeProcessEvents中被调用。不同的是文件事件底层委托给 select,epoll等多路复用接口。而时间事件通过每个tick检查时间事件的触发时间是否已经到期。redis4.0版本中只注册了一个时间事件serverCron,它在initServer中注册,在每次aeProcessEvents函数末尾被调用。上文已经提到aeMain函数是redis的事件主循环,它会不断地调用aeProcessEvents

expire指令在server->expires字典dict中插入sds内部数据类型的key值和到期时间,并触发键空间事件。在serverCron中的databaseCron函数中调用activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW)随机抽取expires中的键值,如果过期,则在server->dict中删除对应的键值。

正文

时间事件注册

首先我们观察一下时间事件的结构体,虽然结构体中有许多成员,但可以说实际用到的就when_secwhen_mstimeProc3个成员还有timeProc的返回值。我们可以观察到aeTimeProc会返回一个int类型的值,如果不为-1,会作为下次调用的间隔时间。

/* Time event structure */
typedef struct aeTimeEvent {
long long id; /* time event identifier. */
long when_sec; /* seconds */
long when_ms; /* milliseconds */
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc;
void *clientData;
struct aeTimeEvent *prev;
struct aeTimeEvent *next;
} aeTimeEvent;
typedef int aeTimeProc(struct aeEventLoop *eventLoop, long long id, void *clientData);

注册的函数位于initServer中,aeCreateTimeEvent函数会生成一个aeTimeEvent对象,并将其赋值给eventLoop->timeEventHead

    if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}

时间事件触发

真正处理时间事件的函数是processTimeEvents,但我们回到aeProcessEvents中学习redis中的一个小技巧。aeSearchNearestTimer会找到距离最近的时间事件。如果有(正常情况下肯定会有一个serverCron函数),那么会将距离下一次时间事件的间隔事件写入tvp参数,在aeApiPoll参数中会传入tvp,如果一直没有文件事件触发,那么aeApiPoll函数会等待恰当的时间返回,函数返回后刚好可以处理时间事件。

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
int j;
aeTimeEvent *shortest = NULL;
struct timeval tv, *tvp; shortest = aeSearchNearestTimer(eventLoop);
if (shortest) {
long now_sec, now_ms;
aeGetTime(&now_sec, &now_ms);
tvp = &tv; /* How many milliseconds we need to wait for the next
* time event to fire? */
long long ms =
(shortest->when_sec - now_sec)*1000 +
shortest->when_ms - now_ms; if (ms > 0) {
tvp->tv_sec = ms/1000;
tvp->tv_usec = (ms % 1000)*1000;
} else {
tvp->tv_sec = 0;
tvp->tv_usec = 0;
}
} numevents = aeApiPoll(eventLoop, tvp); for (j = 0; j < numevents; j++) {
//process file events
} /* Check time events */
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop); return processed; /* return the number of processed file/time events */
}

processTimeEvents函数中,会遍历之前注册的函数,如果时间条件满足,则会调用对应的函数。如果函数返回的值不是-1,意味着函数将会利用返回值作为下一次调用函数的间隔时间。ServerCron的频率定义在server.hz,表示一秒钟调用几次serverCron函数,默认是10次/秒。

expire命令

在了解expire命令之前,我们先回顾一下前文的内容,在 文件事件处理过程中,redis会将querybuf中的内容转化为client->argcclient->argv,方式是通过createStringObject转化为对应的字符串类型的对象,因此,argvredisObject的编码类型只可能是embstr或者是raw

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}

如果传递的time to live参数是负数,那么exipre指令会被转化为del指令,直接删除对应的键值。

否则在server->expire内部数据类型dict中添加对应的到期时间。

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) {
addReply(c,shared.czero);
return;
} if (when <= mstime() && !server.loading && !server.masterhost) {
robj *aux; int deleted = dbSyncDelete(c->db,key);
serverAssertWithInfo(c,key,deleted);
server.dirty++; aux = 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;
}
}

删除过期键值

删除过期键值的方式有3种:定时删除,定期删除,被动删除。redis结合使用了定期删除和被动删除。

被动删除

在客户端向服务端发送getexpire等请求时,会调用expireIfNeeded(c->db,c->argv[j]);函数删除过期的键值。令人好奇的是del请求也会调用expireIfNeeded,也就是有可能调用2次dbSyncDelete函数。

主动删除/定期删除

在前文提到的时间事件serverCron函数中,如果不是从库并且开启了active_expire_enabled(默认开启),则会调用activeExpireCycle函数主动清理过期的键值。

默认情况下,CRON_DBS_PER_CALL的值为16,也是dbnum的值,意味着activeExpireCycle一次会处理16个数据库。而且如果上次调用超时,也会按照一次处理dbnum的数据库处理。

并且对每个数据库至少会进行一轮处理,一轮处理中抽取20个样本,如果样本过期,则删除该键。而且如果样本中过期的键超过25%并且没有超时,则会继续迭代,再进行一轮处理。

timelimit的单位是微秒,如果对当前db处理的过程中超时,那么处理之后的db只进行一轮处理。

所以定期删除并不会将所有的过期键删除,在服务器正常运行的情况下,过期键会维持在25%以内。

void activeExpireCycle(int type) {
//静态全局变量
static unsigned int current_db = 0; /* Last DB tested. */
static int timelimit_exit = 0; /* Time limit hit in previous call? */ int j, iteration = 0;
int dbs_per_call = CRON_DBS_PER_CALL;
long long start = ustime(), timelimit; //一次处理多少db
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum; //时间限制,如果总的时间超过限制,则只处理一轮当前的db
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. */ for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
int expired;
redisDb *db = server.db+(current_db % server.dbnum);
current_db++; do {
unsigned long num, slots;
long long now, ttl_sum;
int ttl_samples;
iteration++; if ((num = dictSize(db->expires)) == 0) {
db->avg_ttl = 0;
break;
}
slots = dictSlots(db->expires);
now = mstime(); expired = 0; if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; while (num--) {
dictEntry *de; if ((de = dictGetRandomKey(db->expires)) == NULL) break;
if (activeExpireCycleTryExpire(db,de,now)) expired++;
}
total_ += expired; 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;
}
} } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
}

参考文献

redis 文档

《Redis设计与实现》

自顶向下redis4.0(4)时间事件与expire的更多相关文章

  1. 自顶向下redis4.0(5)持久化

    redis4.0的持久化 目录 redis4.0的持久化 简介 正文 rdb持久化 save命令 bgsave命令 rdb定期保存数据 进程结束保存数据 aof持久化 数据缓冲区 刷新数据到磁盘 ap ...

  2. 自顶向下redis4.0(2)文件事件与客户端

    redis4.0的文件事件与客户端 目录 redis4.0的文件事件与客户端 简介 正文 准备阶段 接受客户端连接 处理数据 返回数据结果 参考文献 简介 文件事件的流程大概如下: 在服务器初始化时生 ...

  3. 自顶向下redis4.0(1)启动

    redis4.0的启动流程 目录 redis4.0的启动流程 简介 正文 全局server对象 初始化配置 初始化服务器 事件主循环 参考文献 简介 redis 在接收客户端连接之前,大概做了以下几件 ...

  4. 自顶向下redis4.0(3)命令与dict

    redis4.0的命令 简介 目录 redis4.0的命令 简介 正文 redisCommand与redisCommandTable 初始化命令 执行命令 set指令与字典 参考文献 正文 redis ...

  5. redis-4.0.8 配置文件解读

    # Redis configuration file example.## Note that in order to read the configuration file, Redis must ...

  6. Redis4.0 主从复制(PSYN2.0)

    Redis4.0版本相比原来3.x版本,增加了很多新特性,如模块化.PSYN2.0.非阻塞DEL和FLUSHALL/FLUSHDB.RDB-AOF混合持久化等功能.尤其是模块化功能,作者从七年前的re ...

  7. redis4.0.10安装与常用命令

    ----------- redis安装 ------------------------------------------- 安装reids:https://redis.io/download (4 ...

  8. Redis4.0支持的新功能说明

    本文以华为云DCS for Redis版本为例,介绍Redis4.0的新功能.文章转载自华为云帮助中心. 与Redis3.x版本相比,DCS的Redis4.x以上版本,除了开源Redis增加的特性之外 ...

  9. Redis4.0新特性(一)-Memory Command

    Redis4.0版本增加了很多诱人的新特性,在redis精细化运营管理中都非常有用(猜想和antirez加入redislabs有很大关系):此系列几篇水文主要介绍以下几个新特性的使用和效果. Redi ...

随机推荐

  1. .NET 开源工作流: Slickflow流程引擎高级开发(八) -- 审批网关(ApprovalOrSplit)模式的应用

    前言:业务流程流转过程中,审批类型的节点是比较常见的,在审批操作中,常见的操作就是就是主管人员对待办事项进行同意或者拒绝.所以网关处理节点,就是需要对这两种审批结果进行预备处理,审批网关是在或分支(O ...

  2. pikachs 渗透测试1-环境及暴力破解

    一.安装 PhpStudy20180211,默认安装 1.mysql默认密码是root,因为在虚拟机,保留不动 2.解压pikachs 到 C:\phpStudy\PHPTutorial\WWW\pi ...

  3. Java8常用的内置函数式接口(一)Predicate、Consumer、Supplier、Function

    Java8常用的内置函数式接口(一) 简介 JDK 1.8 API中包含了很多内置的函数式接口.有些是在以前版本的Java中大家耳熟能详的,例如Comparator接口,或者Runnable接口.对这 ...

  4. 25. K 个一组翻转链表

    给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表.k 是一个正整数,它的值小于或等于链表的长度.如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序.示例 :给定这个链表: ...

  5. C++实现任意进制的相互转换

    进制转换是计算机内部时时刻刻都在进行活动,本篇文章也是进制转换的算法介绍,不过不同的是我想利用ascll编码实现2到61之间任意进制的相互转换,更大进制的表示方法只不过是十六进制表示方法的延伸:用字母 ...

  6. python大数问题

    python不需要特殊的声明,可以直接进行大数运算 验证:

  7. jenkins.war

    一准备工作 首先你得打开SSH 二将jenkins.war转移到jenkins.war /usr/local/tomcat/apache-tomcat-7.0.63/webapps/中 然后启动tom ...

  8. PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

    注:网上搜来的快照,暂未验证 在java代码中请求https链接的时候,可能会报下面这个错误javax.net.ssl.SSLHandshakeException: sun.security.vali ...

  9. Python中sorted(iterable, *, key=None, reverse=False)函数参数定义中的独立星号(*)的含义

    老猿在 <Python中函数的参数带星号是什么意思?>中介绍了Python函数中参数带星号的含义,而在实际使用和Python的标准文档中,会看到某写函数(如sorted(iterable, ...

  10. 第11.1节 Python正则表达式概述

    正则表达式是可匹配文本片段的模式,一个正则表达式指定了一个与之匹配的字符串集合.最简单的正则表达式为普通字符串,与它自己匹配.如正则表达式'python'与字符串'python'匹配.通过匹配,可以在 ...