Redis 事件

329 阅读6分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

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

  • 文件事件:Redis 服务器通过套接字与客户端进行连接,而文件事件就是服务器对套接字操作的抽象
  • 时间事件:Redis 服务器中的一些操作需要在给定的时间点执行

文件事件

Redis 是基于 Reactor 模式开发了自己的网络事件处理器,这个处理器被称为文件事件处理器:

  • 文件事件处理器使用 I/O 多路复用(multiplexing) 程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字关联好的事件处理器进行处理

文件事件处理器的构成

文件事件处理器分为四部分: 套接字、I/O 多路复用程序、文件事件分派器、事件处理器。

尽管多个文件事件可能会并发的出现,但 I/O 多路复用程序总会将所有产生事件的套接字放入到一个队列中,然后通过队列,以有序、同步、每次一个套接字的方式向文件事件分派器中传送套接字。当上一个执行完毕后,才会继续发送下一个套接字。

I/O 多路复用程序的实现

Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select、epoll、evport和kqueue这些I/O多路复用函数库来实现的,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件。

Redis 在I/O多路复用程序的实现源码中用宏定义了相应的规则,程序会在编译时自动选择系统中性能最高的I/O多路复用函数库来作为 Redis 的底层实现。

#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

文件事件处理器

Redis 为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通行需求。

  • 为了对连接服务器的各个客户端进行应答,服务器要为监听套接字关联连接应答处理器
  • 为了接收客户端传来的命令请求,服务器要为监听套接字关联命令请求处理器
  • 为了向客户端返回命令的执行结果,服务器要为监听套接字关联命令回复处理器
  • 当主服务器和从服务器进行复制操作时,主从服务器都需要关连特别为复制功能编写的复制处理器

时间事件

Redis 时间事件分为两类:

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

一个时间事件主要由三个属性组成:

  • id: 服务器全部唯一ID
  • when: 时间事件的到达时间
  • timeProc: 时间事件处理函数

实现

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

/* 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;
    int refcount; /* refcount to prevent timer events from being
         * freed in recursive time event calls. */
} aeTimeEvent;

aeTimeEvent.png

应用实例:serverCron 函数

持续运行的 Redis 服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定的运行,这些定期操作由 server.c/serverCron 函数负责执行,主要工作包括:

  • 更新服务器的各类统计信息
  • 清理数据库中的过期键值对
  • 关闭和清理连接失效的客户端
  • 尝试进行 AOF 或 RDB 持久化操作
  • 如果服务器是主服务器,那么对从服务器进行定期同步
  • 如果处于集群模式,对集群进行定期同步和连接测试

事件的调度与执行

因为服务器中同时存在文件事件和时间事件两种事件类型,所以服务器必须对两种事件进行调度,决定何时应该处理文件事件、何时应该处理时间事件,以及花多长时间来处理。

事件的调度和执行由 ae.c/aeProcessEvents 函数负责

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

        if (eventLoop->flags & AE_DONT_WAIT) {
            tv.tv_sec = tv.tv_usec = 0;
            tvp = &tv;
        }

        if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)
            eventLoop->beforesleep(eventLoop);

        /* 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 later. 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 fsyncing 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++;
                fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
            }

            /* 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 = &eventLoop->events[fd]; /* Refresh in case of resize. */
                if ((fe->mask & mask & AE_READABLE) &&
                    (!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 */
}