redis是一种典型的reactor设计模式。redis中的任何操作甚至是定时任务的触发都被看成是一个事件,由专门的事件处理器去执行。其内部自己定义了一套ae驱动器(ae.h、ae.c)。
事件分为文件事件和时间事件。像命令的请求,回复都是文件事件。而时间事件则是一种定时触发的事件,如serverCron函数(周期时间事件)、或者过期键的删除(定时时间事件)等。
服务器将所有的时间事件都放在了一个无序链表,每当时间事件执行器运行时,就会遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。定时时间事件在执行的时候会先判断当前时间是否到了,若还没到则会把事件重新加入时间事件链表等待下次的触发。而周期时间事件则是在执行之后会新建一个时间事件并且指定下次触发的时间加入到时间事件链表。
其实无论是时间事件还是文件事件都会用链表去描述。具体见
aeEventLoop
redis只需循环处理aeEventLoop上的事件即可。而这个循环的入口就是
aeMain
上图不难看出,事件处理逻辑主要在aeProcessEvents
除aeMain和aeProcessEvents外,ae其他常用api如下
api | 说明 |
---|---|
aeCreateEventLoop | 初始化一个事件循环结构体(eventLoop) |
aeGetSetSize | 返回当前setsize的值 |
aeResizeSetSize | 改变setsize的值(重新分配空间) |
aeDeleteEventLoop | 删除事件循环,释放内存空间 |
aeStop | 停止事件循环,即stop值设为1 |
aeProcessEvents | 事件处理逻辑 |
aeMain | 事件循环的入口 |
aeSetBeforeSleepProc | 注册回调函数,每次主循环在休眠前被调用 |
aeSetAfterSleepProc | 注册回调函数,每次主循环在休眠后被调用 |
aeWait | 等待指定套接字指定事件的产生 |
aeCreateFileEvent | 创建一个文件事件 |
aeCreateTimeEvent | 创建一个时间事件 |
以客户端发送的命令为例,当套接字收到客服端发来的请求时,会调用aeCreateFileEvent创建一个文件事件并加入到aeEventLoop的事件链表中,由aeMain的事件循环中调用指定的事件处理器去处理该事件。
那么问题来了,事件循环只有一个但客户端可以有多个。换句话说就是redis是如何做到在一个线程里面高效的处理多个客户端的请求的呢?其秘密武器就是多路复用的IO模型——NIO。
如下图所示redis内部对不同操作系统所能支持的nio模型做了不同的实现,但对外暴露的api是一致的。这么做的好处就是nio内部的实现对于调用方来说可以是黑盒,无需关心其内部的具体实现。
api | 描述 |
---|---|
aeApiCreate | 创建底层apidata(如:epoll_create) |
aeApiResize | 重新分配底层ae事件内存 |
aeApiFree | 关闭底层ae并释放内存 |
aeApiAddEvent | 发起一个事件 |
aeApiDelEvent | 删除一个事件 |
aeApiPoll | 查询发生的监听事件 |
aeApiName | 返回实现类型(select/epoll/evport/kqueue) |
具体什么系统用什么实现,可以在ae.c找到答案
我们点进去开就能发现linux系统用的是epoll的实现。
说了这么多,那么到底aeMain和nio有什么关系呢? 其实aeMain是以nio的为基础的事件响应框架,而nio是aeMain的底层实现。
以常用的epoll模型为例,aeCreateFileEvent方法调用了aeApiAddEvent。(aeApiAddEvent正是redis对nio实现所对外暴露的api之一)
点开aeApiAddEvent可以发现其内部调用的正是epoll_ctl这个系统函数
同理aeApiPoll调用的是epoll_wait
好了,那么问题又来了,为什么nio会高效呢?其实现高效的原理是什么?
其实这个高效是操作系统自己实现的(毕竟调用的就是系统函数)。就以epoll为例,其内部的实现有几个关键字:零拷贝(共享内存空间) + 红黑树 + 链表有兴趣的可以去了解一下。
最后再多说一句,除了redis的ae外,像netty、libevent也是一样基于nio实现的事件响应框架。有兴趣也可以去了解一下。
参考文献:《Redis设计与实现》黄健宏著、redis-5.0.14源码