Redis事件及源码分析

1,228 阅读6分钟

事件类型

Redis服务器是一个事件驱动程序,服务器需要处理以下两种事件:

  1. 文件事件:Redis服务器通过套接字与客户端(或其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。
  2. 时间事件:Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。

文件事件

I/O多路复用

Redis为每个I/O多路复用函数库都实现了相同的API,所以I/O多路复用程序的底层实现是可以互换的。

Redis编译时自动根据系统中性能最高的I/O多路复用函数库来作为Redis的I/O多路复用程序的底层实现。在源码文件ae.c中能看到以下代码:

/* Include the best multiplexing layer supported by this system.
 * The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

文件事件类型

  1. 可读事件:定义在ae.h文件中的AE_READABLE事件。当套接字变得可读时(客户端对套接字执行write操作或close操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connect操作),套接字产生AE_READABLE事件。
  2. 可写事件:定义在ae.h文件中的AE_WRITABLE事件。当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE事件。

时间事件

时间事件类型

  1. 定时事件:让一段程序在指定的时间之后执行一次。
  2. 周期性事件:让一段程序每隔指定时间就执行一次。

目前Redis只使用周期性事件,没有使用定时事件。

时间事件属性

  1. id:全局唯一id,按从小到大的顺序递增,新事件的id比旧事件的id大。
  2. when:毫秒精度的UNIX时间戳,记录了事件的到达时间。
  3. timeProc:时间事件处理器,是一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。

实现

服务器将所有时间事件放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

新的事件总是插入到链表的表头,所以事件按id逆序排序。因为链表没有按when属性进行排序,所以当时间事件执行器运行的时候,它必须遍历链表中的所有时间事件,这样才能确保服务器中所有已到达的时间事件都会被处理,时间复杂度O(n)。

源码分析

下载的redis版本是5.0.5,解压完后,源码文件在src目录中。

server.c中的main函数是Redis服务器程序执行的入口。main函数执行了很多工作,例如加载配置文件,初始化服务器等等。在这里我们主要关注aeMain函数,这是定义在ae.c文件中的函数,会在Redis服务器main函数中调用。aeMain函数长这样:

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
    }
}

在这里,我们讲讲aeProcessEvents函数,这个函数也是定义在ae.c文件中,贴出代码:

/* Process every pending time event, then every pending file event
 * (that may be registered by time event callbacks just processed).
 * Without special flags the function sleeps until some file event
 * fires, or when the next time event occurs (if any).
 *
 * If flags is 0, the function does nothing and returns.
 * if flags has AE_ALL_EVENTS set, all the kind of events are processed.
 * if flags has AE_FILE_EVENTS set, file events are processed.
 * if flags has AE_TIME_EVENTS set, time events are processed.
 * if flags has AE_DONT_WAIT set the function returns ASAP until all
 * if flags has AE_CALL_AFTER_SLEEP set, the aftersleep callback is called.
 * the events that's possible to process without to wait are processed.
 *
 * The function returns the number of events processed. */
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    /* Nothing to do? return ASAP */
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

    /* Note that we want call select() even if there are no
     * file events to process as long as we want to process time
     * events, in order to sleep until the next time event is ready
     * to fire. */
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;

        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;

            /* 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;
            }
        } 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 */
            }
        }

        /* Call the multiplexing API, will return only on timeout or when
         * some event fires. */
        numevents = aeApiPoll(eventLoop, tvp);

        /* After sleep callback. */
        if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
            eventLoop->aftersleep(eventLoop);

        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 fired = 0; /* Number of events fired for current fd. */

            /* Normally we execute the readable event first, and the writable
             * event laster. This is useful as sometimes we may be able
             * to serve the reply of a query immediately after processing the
             * query.
             *
             * However if AE_BARRIER is set in the mask, our application is
             * asking us to do the reverse: never fire the writable event
             * after the readable. In such a case, we invert the calls.
             * This is useful when, for instance, we want to do things
             * in the beforeSleep() hook, like fsynching a file to disk,
             * before replying to a client. */
            int invert = fe->mask & AE_BARRIER;

            /* 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.
             *
             * Fire the readable event if the call sequence is not
             * inverted. */
            if (!invert && fe->mask & mask & AE_READABLE) {
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                fired++;
            }

            /* Fire the writable event. */
            if (fe->mask & mask & AE_WRITABLE) {
                if (!fired || fe->wfileProc != fe->rfileProc) {
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }

            /* If we have to invert the call, fire the readable event now
             * after the writable one. */
            if (invert && fe->mask & mask & AE_READABLE) {
                if (!fired || fe->wfileProc != fe->rfileProc) {
                    fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }

            processed++;
        }
    }
    /* Check time events */
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed; /* return the number of processed file/time events */
}

由于代码边幅太长,现把主流程翻译成伪代码:

def aeProcessEvents():
	# 获取到达时间离当前时间最接近的时间事件
    time_event = aeSearchNearestTimer()
    
    # 计算最接近的时间事件距离到达还有多少毫秒
    remain_ms = time_event.when - unix_ts_now()
    
    # 如果事件已到达,那么remain_ms的值可能为负,将它设定为0
    if remain_ms < 0:
    	remain_ms = 0
    
    # 根据remain_ms的值,创建timeval结构
    timeval = create_timeval_with_ms(remain_ms)
    
    # 阻塞并等待文件事件产生,最大阻塞时间由传入的timeval结构决定
    # 如果remain_ms的值为0,那么aeApiPoll调用之后马上返回,不会阻塞
    aeApiPoll(timeval)
    
    # 处理所有已经产生的文件事件
    # 事实上并没有processFileEvents函数,相关代码是写在aeProcessEvents里
    processFileEvents()
    
    # 处理所有已经到达的时间事件
    processTimeEvents()

aeApiPoll()

在aeApiPoll中会调用多路复用函数,具体调用哪个实现的多路复用函数,在编译时已经确定。例如。在ae_select.c和ac_epoll.c中都有aeApiPoll函数,现贴出ac_epoll.c中的实现:

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
    if (retval > 0) {
        int j;

        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

规则总结

  1. aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这个方法既可以避免服务器对时间事件频繁的轮询,也可以确保aeApiPoll函数不会阻塞过长时间。
  2. 因为文件事件时随机出现的,如果等待并处理完一次文件事件后,仍未有时间事件到达,那么服务器将会再次等待并处理文件事件。随着文件事件的不断执行,时间会逐渐向时间事件设定的时间逼近,并最终来到到达时间,这时服务器就可以开始处理到达的时间事件了。
  3. 对文件事件和时间事件的处理时同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占。
  4. 因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一点。

参考

  • 黄健宏的《Redis设计与实现》
  • Redis5.0.5源码