1、redis事件介绍
Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件
- 文件事件:Redis服务器通过套接字与客户端(或者其他Redis服务器进行连接),而文件事件就是服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
- 时间事件:Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行。而事件时间就是服务器对这类定时操作的抽象。
2、事件分析
2.1、事件处理框架
redis中对事件处理的过程如下图
上图中的I/O多路复用程序也相当于一个队列,如下图
2.2、源码分析
首先Redisservice服务启动调用的是redis.c文件中的main函数,main函数部分挺长,我们先不关注其他部分,只关心redis是如何启动第一个时间事件和文件事件的。
main函数中调用了initServer()函数,这时最重要的启动服务的函数,这里为了突出所要讨论的要点,删去了一些代码
什么时候如何创建时间事件和文件事件
void initServer() {
int j;
........此处删除一部分代码
/* 这个server.port的默认值是6379,是默认的服务端开启监听的端口*/
if (server.port != 0) {
/* anetTcpserver函数开启监听*/
server.ipfd = anetTcpServer(server.neterr,server.port,server.bindaddr);
if (server.ipfd == ANET_ERR) {
redisLog(REDIS_WARNING, "Opening port %d: %s",
server.port, server.neterr);
exit(1);
}
}
/* 尝试建立unix下的网络监听*/
if (server.unixsocket != NULL) {
unlink(server.unixsocket); /* don't care if this fails */
server.sofd = anetUnixServer(server.neterr,server.unixsocket,server.unixsocketperm);
if (server.sofd == ANET_ERR) {
redisLog(REDIS_WARNING, "Opening socket: %s", server.neterr);
exit(1);
}
}
/* 如果windows下和unix下都没有成功建立监听端口,则失败返回*/
if (server.ipfd < 0 && server.sofd < 0) {
redisLog(REDIS_WARNING, "Configured to not listen anywhere, exiting.");
exit(1);
}
/* 创建redis的database并初始化他的内部参数*/
for (j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&dbDictType,NULL);
server.db[j].expires = dictCreate(&keyptrDictType,NULL);
server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
server.db[j].ready_keys = dictCreate(&setDictType,NULL);
server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
server.db[j].id = j;
}
/*初始化参数*/
server.pubsub_channels = dictCreate(&keylistDictType,NULL);
server.pubsub_patterns = listCreate();
listSetFreeMethod(server.pubsub_patterns,freePubsubPattern);
listSetMatchMethod(server.pubsub_patterns,listMatchPubsubPattern);
.....省略一部分代码
server.lastbgsave_status = REDIS_OK;
/* 开启时间事件,详细见下面aeCreateTimeEvent函数分析*/
if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
redisPanic("create time event failed");
exit(1);
}
/* 开启文件事件,这边第一个文件事件是建立连接的事件,调用的是acceptTcpHandler函数
详细函数分析见下面aeCreateFileEvent函数分析*/
if (server.ipfd > 0 && aeCreateFileEvent(server.el,server.ipfd,AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR) redisPanic("Unrecoverable error creating server.ipfd file event.");
if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
acceptUnixHandler,NULL) == AE_ERR) redisPanic("Unrecoverable error creating server.sofd file event.");
...省略了一部分代码
}
}
所以就是在main函数中会创建第一次的时间事件和文件事件,其中文件事件是接受tcp连接的事件,下面的函数是上面函数中用到的函数详解
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc)
{
/*这个是表示这个时间事件的排名,是第几个时间事件,这边一开始timeEventNextId为0
所以这边第一个id号为0,后面一次增加*/
long long id = eventLoop->timeEventNextId++;
aeTimeEvent *te;
/* 申请单独的时间事件空间*/
te = zmalloc(sizeof(*te));
if (te == NULL) return AE_ERR;
te->id = id;
/*获取这个时间事件在马上要添加到处理队列时的时间戳,精确到毫秒*/
aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
/*执行函数,一开始默认的函数都为serverCron函数,这个函数用来处理时间事件*/
te->timeProc = proc;
te->finalizerProc = finalizerProc;
te->clientData = clientData;
te->next = eventLoop->timeEventHead;
/*因为这个时间事件是第一个,所以将这个时间事件放在头部*/
eventLoop->timeEventHead = te;
return id;
}
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
/*这个表示最大的文件事件队列的长度,一般为10128*/
if (fd >= eventLoop->setsize) {
errno = ERANGE;
return AE_ERR;
}
aeFileEvent *fe = &eventLoop->events[fd];
/*初始化文件事件,并添加,详见下面相应函数分析*/
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
return AE_ERR;
fe->mask |= mask;
/*将相应的调用函数添加到aeFileEvent的文件事件的变量中*/
if (mask & AE_READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
/*一开始的获取建立连接的文件事件这个clientData为空*/
fe->clientData = clientData;
//更新当前最大的fd
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
/* aeApiState用来标识一个事件是读事件还是写事件*/
aeApiState *state = eventLoop->apidata;
/* 利用mask来标识一个事件的读写性*/
if (mask & AE_READABLE) FD_SET(fd,&state->rfds);
if (mask & AE_WRITABLE) FD_SET(fd,&state->wfds);
return 0;
}
何时处理时间事件和文件事件
上面我们可以看到创建时间事件和文件事件的过程,但是redis何时会处理这些事件呢?
我们需要再回到main函数,再main函数中还会调用aeMain(server.el);
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
/*在这个函数中会不断循环调用aeProcessEvents函数来处理时间事件*/
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* 如果既不是时间事件也不是文件事件则直接返回,因为redis中只有这两种事件机制 */
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
/*1、如果是前者判断为1,后者判断为0,说明是文件事件存入,但此次不是时间事件,进入处理
2、如果前者判断为1, 后者判断为1,说明是时间事件,也进入处理
3、如果前者为0,说明还没有事件发生,则不进入*/
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
int j;
aeTimeEvent *shortest = NULL;
struct timeval tv, *tvp;
/*判断是时间事件,则redis是会寻找最近的一次事件事件,因为事件都是存放
在eventLoop环境中,可以直接读取大到,所以将读取到的最近的一次
时间事件放到shortest中
*/
if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
/*详细见下面函数介绍*/
shortest = aeSearchNearestTimer(eventLoop);
if (shortest) {
long now_sec, now_ms;
/* 记录调用时间事件之前的时间戳,这样可以在调用后的时间戳进行比较,从而获取调用这个时间事件函数所花费的时间 */
aeGetTime(&now_sec, &now_ms);
tvp = &tv;
tvp->tv_sec = shortest->when_sec - now_sec;
//判断当前时间是否达到最近的时间事件的时间点
if (shortest->when_ms < now_ms) {
tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
tvp->tv_sec --;
} else {
tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
}
if (tvp->tv_sec < 0) tvp->tv_sec = 0;
if (tvp->tv_usec < 0) tvp->tv_usec = 0;
} else {
/* If we have to check for events but need to return
* ASAP because of AE_DONT_WAIT we need to set the timeout
* to zero */
if (flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
/* Otherwise we can block */
tvp = NULL; /* wait forever */
}
}
/*返回可以进行读写的事件的信息,信息在eventLoop->fire数组中,numevents表示有几个事件可以进行读写
当然这边处理的文件事件,是用来处理I/O操作的,时间事件的操作在下面的processTimeEvents函数中*/
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;
/* note the fe->mask & mask & ... code: maybe an already processed
* event removed an element that fired and we still didn't
* processed, so we check if the event is still valid.
可以看出这样设计的使用来保证运行时事件是否还是有效的,如果有效则调用相应的事件函数 */
if (fe->mask & mask & AE_READABLE) {
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
processed++;
}
}
/* Check time events */
if (flags & AE_TIME_EVENTS)
/*processTimeEvents函数是用来处理时间事件的函数
也解决了用来处理如果在计算机中系统事件突然调快了,然后再调回正常
这种情况可能会造成一些时间函数被无限期的延迟运行,这个函数的解决方式是保证在时间调回正常时
尽可能早的运行这些函数*/
processed += processTimeEvents(eventLoop);
return processed; /* return the number of processed file/time events */
}
static aeTimeEvent *aeSearchNearestTimer(aeEventLoop *eventLoop)
{
/*寻找最近的一次时间事件,这里是通过遍历来获取,因为事件事件都是存放在EeventLoop的
timeEventHead列表中*/
aeTimeEvent *te = eventLoop->timeEventHead;
aeTimeEvent *nearest = NULL;
while(te) {
if (!nearest || te->when_sec < nearest->when_sec ||
(te->when_sec == nearest->when_sec &&
te->when_ms < nearest->when_ms))
nearest = te;
te = te->next;
}
return nearest;
}
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, j, numevents = 0;
memcpy(&state->_rfds,&state->rfds,sizeof(fd_set));
memcpy(&state->_wfds,&state->wfds,sizeof(fd_set));
/*select函数可以从eventLoop->maxfd+1这些文件描述符的集合中探测出有几个文件描述符对应的文件发生了变化,retval则表示了发生变化的文件数量*/
retval = select(eventLoop->maxfd+1,
&state->_rfds,&state->_wfds,NULL,tvp);
/*如果retval大于0,则通过遍历来获得发生变化的文件描述符下标和记录对应文件描述符的可读写性
可以看到如果对应的文件的时间为none,则直接continue
最后结果保存在eventLoop->fired数组中*/
if (retval > 0) {
for (j = 0; j <= eventLoop->maxfd; j++) {
int mask = 0;
aeFileEvent *fe = &eventLoop->events[j];
if (fe->mask == AE_NONE) continue;
if (fe->mask & AE_READABLE && FD_ISSET(j,&state->_rfds))
mask |= AE_READABLE;
if (fe->mask & AE_WRITABLE && FD_ISSET(j,&state->_wfds))
mask |= AE_WRITABLE;
eventLoop->fired[numevents].fd = j;
eventLoop->fired[numevents].mask = mask;
numevents++;
}
}
return numevents;
}
2.3、总结
可以结合2.1中看到事件机制就是一个不断放,不断去的过程。每当事件变成可应答、可写或者可读的时候相应的文件事件就会发生,记录到eventLoop中。文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件过程中也不糊发生抢占。