redis 采用事件驱动机制来处理大量的网络IO。它并没有使用 libevent 或者 libev 这样的成熟开源方案,而是自己实现一个非常简洁的事件驱动库 ae_event。
Redis中的事件驱动库只关注网络IO,以及定时器。该事件库处理下面两类事件:
事件驱动库的代码主要是在src/ae.c中实现的,其示意图如下所示。
aeEventLoop 是整个事件驱动的核心,它管理着文件事件表和时间事件列表,不断地循环处理着就绪的文件事件和到期的时间事件。下面我们就先分别介绍文件事件和时间事件,然后讲述相关的 aeEventLoop 源码实现。
文件事件
Redis基于Reactor模式开发了自己的网络事件处理器,也就是文件事件处理器。文件事件处理器使用IO多路复用技术,同时监听多个套接字,并为套接字关联不同的事件处理函数。当套接字的可读或者可写事件触发时,就会调用相应的事件处理函数。
Redis 使用的IO多路复用技术主要有: select 、 epoll 、 evport 和 kqueue等。每个IO多路复用函数库在 Redis 源码中都对应一个单独的文件,比如ae select.c,ae epoll.c, ae_kqueue.c等。Redis 会根据不同的操作系统,按照不同的优先级选择多路复用技术。事件响应框架一般都采用该架构,比如 netty 和 libevent。
如下图所示,文件事件处理器有四个组成部分,它们分别是套接字、I/O多路复用程序、文件事件分派器以及事件处理器。
文件事件是对套接字操作的抽象,每当一个套接字准备好执行 accept、read、write和 close 等操作时,就会产生一个文件事件。因为 Redis 通常会连接多个套接字,所以多个文件事件有可能并发的出现。
I/O多路复用程序负责监听多个套接字,并向文件事件派发器传递那些产生了事件的套接字。
尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生的套接字都放到同一个队列(也就是后文中描述的 aeEventLoop 的 fired 就绪事件表)里边,然后文件事件处理器会以有序、同步、单个套接字的方式处理该队列中的套接字,也就是处理就绪的文件事件。
所以,一次 Redis 客户端与服务器进行连接并且发送命令的过程如上图所示。
时间事件
Redis 的时间事件分为以下两类:
Redis 的时间事件的具体定义结构如下所示。
一个时间事件是定时事件还是周期性事件取决于时间处理器的返回值:
Redis 将所有时间事件都放在一个无序链表中,每次 Redis 会遍历整个链表,查找所有已经到达的时间事件,并且调用相应的事件处理器。
介绍完文件事件和时间事件,我们接下来看一下 aeEventLoop 的具体实现。
创建事件管理器
Redis 服务端在其初始化函数 initServer 中,会创建事件管理器 aeEventLoop 对象。
函数 aeCreateEventLoop 将创建一个事件管理器,主要是初始化 aeEventLoop 的各个属性值,比如 events 、 fired 、 timeEventHead 和 apidata :
aeApiCreate 函数首先创建了 aeApiState 对象,初始化了epoll就绪事件表;然后调用 epoll_create 创建了 epoll 实例,最后将该 aeApiState 赋值给 apidata 属性。
aeApiState 对象中 epfd 存储 epoll 的标识, events 是一个 epoll 就绪事件数组,当有 epoll 事件发生时,所有发生的 epoll 事件和其描述符将存储在这个数组中。这个就绪事件数组由应用层开辟空间、内核负责把所有发生的事件填充到该数组。
创建文件事件
aeFileEvent 是文件事件结构,对于每一个具体的事件,都有读处理函数和写处理函数等。Redis 调用 aeCreateFileEvent 函数针对不同的套接字的读写事件注册对应的文件事件。
比如说,Redis 进行主从复制时,从服务器需要主服务器建立连接,它会发起一个 socekt连接,然后调用 aeCreateFileEvent 函数针对发起的socket的读写事件注册了对应的事件处理器,也就是 syncWithMaster 函数。
aeCreateFileEvent 的参数 fd 指的是具体的 socket 套接字, proc 指 fd 产生事件时,具体的处理函数, clientData 则是回调处理函数时需要传入的数据。 aeCreateFileEvent 主要做了三件事情:
如上文所说,Redis 基于的底层 I/O 多路复用库有多套,所以 aeApiAddEvent 也有多套实现,下面的源码是 epoll 下的实现。其核心操作就是调用 epoll 的 epoll_ctl 函数来向 epoll 注册响应事件。有关 epoll 相关的知识可以看一下《JAVA NIO源码分析》
事件处理
因为 Redis 中同时存在文件事件和时间事件两个事件类型,所以服务器必须对这两个事件进行调度,决定何时处理文件事件,何时处理时间事件,以及如何调度它们。
aeMain 函数以一个无限循环不断地调用 aeProcessEvents 函数来处理所有的事件。
下面是 aeProcessEvents 的伪代码,它会首先计算距离当前时间最近的时间事件,以此计算一个超时时间;然后调用 aeApiPoll 函数去等待底层的I/O多路复用事件就绪; aeApiPoll函数返回之后,会处理所有已经产生文件事件和已经达到的时间事件。
与 aeApiAddEvent 类似, aeApiPoll 也有多套实现,它其实就做了两件事情,调用 epoll_wait 阻塞等待 epoll 的事件就绪,超时时间就是之前根据最快达到时间事件计算而来的超时时间;然后将就绪的 epoll 事件转换到fired就绪事件。 aeApiPoll 就是上文所说的I/O多路复用程序。具体过程如下图所示。
processFileEvent 是处理就绪文件事件的伪代码,也是上文所述的文件事件分派器,它其实就是遍历 fired 就绪事件表,然后根据对应的事件类型来调用事件中注册的不同处理器,读事件调用 rfileProc ,而写事件调用 wfileProc 。
而 processTimeEvents 是处理时间事件的函数,它会遍历 aeEventLoop 的事件事件列表,如果时间事件到达就执行其 timeProc 函数,并根据函数的返回值是否等于 AE_NOMORE来决定该时间事件是否是周期性事件,并修改器到达时间。
删除事件
当不在需要某个事件时,需要把事件删除掉。例如: 如果fd同时监听读事件、写事件。当不在需要监听写事件时,可以把该fd的写事件删除。
aeDeleteEventLoop 函数的执行过程总结为以下几个步骤 1、根据 fd 在未就绪表中查找到事件 2、取消该 fd 对应的相应事件标识符 3、调用 aeApiFree 函数,内核会将epoll监听红黑树上的相应事件监听取消。