事件
Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:
文件事件(file event)
Redis服务器通过套接字与客户端进行连接,而文件事件就是服务器对套接字操作的抽象。
服务器与和客户端的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列的网络通信操作。
时间事件(time event)
Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
文件事件
redis基于Reactor模式开发了自己的网络事件处理器;这个处理器被称为文件事件处理器(file event handler)。
文件事件处理器使用IO多路复用程序来同时监听多个套接字,并且根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答、读取、写入、关闭等操作时,与操作相对应的文件事件就会产生,处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行,但通过IO多路复用程序来监听多个套接字,实现了高性能的网络通信模型,保持了单线程设计的简单性。
/* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) 文件事件类型 */
aeFileProc *rfileProc; /* 可读事件回调函数 */
aeFileProc *wfileProc; /* 可写事件回调函数 */
void *clientData; /* 客户端传入的数据 */
} aeFileEvent;
/* A fired event */
typedef struct aeFiredEvent {
int fd; /* 就绪文件句柄 */
int mask; /* 就绪文件事件类型 */
} aeFiredEvent;
时间事件
/* Time event structure 双向链表*/
typedef struct aeTimeEvent {
long long id; /* time event identifier. 时间事件id */
long when_sec; /* seconds 执行节点秒 */
long when_ms; /* milliseconds 执行节点毫秒 */
aeTimeProc *timeProc; /* 时间事件处理函数 */
aeEventFinalizerProc *finalizerProc; /* 时间事件终结函数 */
void *clientData; /* 事件数据 */
struct aeTimeEvent *prev; /* 上一个时间事件 */
struct aeTimeEvent *next; /* 下一个时间事件 */
} aeTimeEvent;
事件循环
typedef struct aeEventLoop {
int maxfd; /* 当前最大的已注册事件文件描述符 */
int setsize; /* 文件描述符监听集合的大小 */
long long timeEventNextId; /* 下一个时间事件id */
time_t lastTime; /* 上一次执行事件的时间 */
aeFileEvent *events; /* 已经注册的文件事件,以fd为索引 */
aeFiredEvent *fired; /* 已经就绪的文件时间,以fd为索引 */
aeTimeEvent *timeEventHead; /* 时间事件的表头 */
int stop; /* 事件处理开关 */
void *apidata; /* used for polling API specific data 事件状态数据,根据不同的多路事件库来选择 */
aeBeforeSleepProc *beforesleep; /* epoll_wait之前调用 */
aeBeforeSleepProc *aftersleep; /* epoll_wait之后调用 */
} aeEventLoop;
epoll 库,*apidata 存储的数据结构如下
typedef struct aeApiState {
int epfd; /* epoll事件描述符 */
struct epoll_event *events; /* epoll事件表 */
} aeApiState;
aeMain 循环执行aeProcessEvents,aeProcessEvents返回处理事件数量
1. 查找最近的一个超时事件 aeSearchNearestTimer
遍历eventLoop->timeEventHead
如果没有计时器,则返回 NULL
查找O(N),因为时间事件未排序。可能的优化(目前 Redis 不需要):
1)按顺序插入事件,使最近的只是头部。更好,但仍然插入或删除计时器是 O(N)。
2) 使用跳跃列表skiplist将此操作作为 O(1) 并将插入作为 O(log(N))
如果有定时器:获得当前时间,计算还需要多久触发事件tvp
如果无定时器:tv = NULL
如果AE_DONT_WAIT: tv = 0
2. 调用多路复用 API aeApiPoll,等待文件事件触发、或超时时返回
aeApiPoll有4种实现:select、kqueue、epoll、evport
aeApiState *state = eventLoop->apidata;
epoll为例:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
timeout参数单位是毫秒,如果指定了大于0的超时时间,则在这段时间内即使如果没有文件事件触发,epoll_wait到了指定时间也会返回。 如果超时时间指定为-1,则epoll_wait将会一直阻塞等待,直到文件事件触发
epoll_wait(state->epfd, state->events, eventLoop->setsize, tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
epoll_wait获得就绪事件描述符,保存到eventLoop->fired就绪事件(fd、mask:AE_READABLE 、AE_WRITABLE),返回就绪事件个数
select为例:
select(eventLoop->maxfd + 1, &state->_rfds, &state->_wfds, NULL, tvp);
遍历小于eventLoop->maxfd的所有fd,判断是否在读事件集合、是否在写事件集合 FD_ISSET,保存到eventLoop->fired就绪事件(fd、mask:AE_READABLE 、AE_WRITABLE)
返回就绪事件个数
3. 处理就绪事件
一般情况下,Redis会先处理读事件(AE_READABLE),再处理写事件(AE_WRITABLE)。
先读后写可以让一个请求的处理和回复都是在同一次循环里面,使得请求可以尽快地回复
如果某个fd的mask包含了AE_BARRIER,那它的处理顺序会是先写后读
遍历eventLoop->fired就绪事件(fd、mask)
调用eventLoop->events[eventLoop->fired.fd]获得文件事件aeFileEvent
根据就绪事件mask调用文件事件aeFileEvent读回调rfileProc或写回调wfileProc
4. 处理时间事件,processTimeEvents
如果当前时间小于上次执行的时间eventLoop->lastTime,需要重置上次事件处理的时间
设置上次时间事件处理的时间eventLoop->lastTime为当前时间
遍历时间事件链表eventLoop->timeEventHead
如果时间事件已经被删除,要从链表中移除
获取当前时间,找到到期时间事件(当前事件 > 时间事件到期时间),回调时间事件函数
创建文件事件
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData)
aeFileEvent *fe = &eventLoop->events[fd]
将某个fd的某些事件及事件回调注册到eventloop中
AE_READABLE 可读事件、AE_WRITABLE 可写事件、AE_BARRIER
epoll为例:调用epoll_ctl EPOLL_CTL_ADD、EPOLL_CTL_MOD
创建时间事件(定时器)
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds, aeTimeProc *proc, void *clientData, aeEventFinalizerProc *finalizerProc)
aeTimeEvent *te
te->timeProc = proc;
te->finalizerProc = finalizerProc;
超时调用回调timeProc,定时器被删除时调用回调finalizerProc
Redis的定时器是一个普通的双向链表,链表也并不是有序的。每次最新的超时事件,直接插入链表的最头部。 当AE要遍历当前时刻的超时事件时,也是直接暴力的从头到尾遍历链表,看有没有超时的事件。
注册事件
aeApiAddEvent,有4种实现:select、kqueue、epoll、evport
向Redis事件表aeEventLoop的event注册一个事件
epoll为例:调用了epoll_ctl
创建事件循环
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR)
1. maxclients代表用户配置的最大连接数,可在启动时由--maxclients指定,默认为10000。
2. CONFIG_FDSET_INCR 大小为128。给Redis预留一些安全空间。
aeEventLoop *aeCreateEventLoop(int setsize)
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize)
setsize参数表示了eventloop可以监听的文件事件的个数,如果当前监听的fd个数超过了setsize,eventloop将不能继续注册。对于eventLoop->events数组来说,fd就是这个数组的下标
aeApiCreate
有4种实现:select、kqueue(支持FreeBSD系统(如macOS) )、epoll(支持Linux系统)、evport(支持Solaris)
epoll为例:epoll_create
对于每种IO复用方式,只要实现以下8个接口就可以正常对接Redis了:
int aeApiCreate(aeEventLoop *eventLoop);
void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask);
void aeApiResize(aeEventLoop *eventLoop, int setsize);
void aeApiFree(aeEventLoop *eventLoop);
int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask);
void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask);
int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp);
char *aeApiName(void);