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. ①SpringCloud 实战:引入Eureka组件,完善服务治理

    简介 Netflix Eureka 是一款由 Netflix 开源的基于 REST 服务的注册中心,用于提供服务发现功能.Spring Cloud Eureka 是 Spring Cloud Netf ...

  2. 面试老被问LinkedList源码?看看阿里技术官是怎么深度剖析的吧!

    前言 LinkedList底层是基于双向链表,链表在内存中不是连续的,而是通过引用来关联所有的元素,所以链表的优点在于添加和删除元素比较快,因为只是移动指针,并且不需要判断是否需要扩容,缺点是查询和遍 ...

  3. IDM下载器的队列功能有什么用?

    使用IDM下载器中的队列功能,可以帮助大家快速分类下载任务,这样,就可以统一管理有同样下载需求的内容. 一.队列的添加及设置 打开IDM下载器,单击菜单中的"队列",可以看到在左侧 ...

  4. 对JVM的一个基础了解

    1.JVM范围 2.JVM和class文件 (1).JVM和Java语言无关,JVM是一种规范,任何语言只要能编译成class文件格式都能在JVM上运行 3.class文件格式 (1).class文件 ...

  5. Jmeter(二十八) - 从入门到精通 - Jmeter Http协议录制脚本工具-Badboy1(详解教程)

    1.简介 在使用jmeter自动录制脚本时会产生很多无用的请求,所以推荐使用badboy录制脚本之后保存为jmx文件,在jmeter中打开使用.因此宏哥在这里介绍一下Badboy这款工具,本来打算不做 ...

  6. sentinel--初级使用篇

    1.官方资料 github官网地址:https://github.com/alibaba/Sentinel wiki:https://github.com/alibaba/Sentinel/wiki/ ...

  7. java多线程--【Foam番茄】

    进程 是系统资源分配的单位 线程 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义.线程是cpu调度和执行的单位 注意:很多多线程是模拟出来的,真正的多线程是指有多 ...

  8. ZAB

    ZAB=ZooKeeper Atomic Broadcast ZooKeeper原子消息广播协议,支持崩溃回复的原子广播协议. zk使用一个单一的主进程来接受并处理客户端的所有事务请求,并采用ZAB的 ...

  9. 【mq读书笔记】消息拉取长轮训机制(Broker端)

    RocketMQ并没有真正实现推模式,而是消费者主动想消息服务器拉取消息,推模式是循环向消息服务端发送消息拉取请求. 如果消息消费者向RocketMQ发送消息拉取时,消息未到达消费队列: 如果不启用长 ...

  10. Django 的缓存机制

    一 缓存介绍: 在动态网站中,用户所有的请求,服务器都会去数据库中进行相应的增,删,查,改,渲染模板,执行业务逻辑,最后生成用户看到的页面. 当一个网站的用户访问量很大的时候,每一次的的后台操作,都会 ...