redis是一个事件驱动程序,服务器需要处理以下两类事件,分别是时间事件和文件事件。redis服务器在启动之后,开始执行事件循环,就可以接受客户端的连接请求并处理客户端发来的命令请求了。
1、服务器初始化
(1)初始化服务器状态结构(initServerConfig函数)
第一步是创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值。初始化服务器的状态主要完成的主要工作包括:
- 设置服务器运行ID(server.runid)
- 设置服务器的serverCron函数的默认运行频率(server.hz)
- 设置服务器的默认配置文件路径(server.configfile)
- 设置服务器的运行架构,32位或者64位(server.arch_bits)
- 设置服务器的默认端口号(server.port)
- 设置服务器默认的AOF持久化条件(server.aof_rewrite_perc、server.aof_rewrite_min_size、server.aof_rewrite_base_size)
- 初始化服务器的LRU时钟(server.lruclock)
- 设置服务器的默认RDB持久化条件(server.saveparams)
- 设置和主从复制相关的状态
- 设置PSYNC命令所使用的backlog
- 设置客户端输出缓冲区限制(server.client_obuf_limits[])
- 调用函数populateCommandTable创建命令表(server.commands)
- 初始化慢查询日志(server.slowlog_log_slower_than、server.slowlog_max_len)
- 初始化调试选项
(2)载入配置选项(loadServerConfig函数)
在初始化server变量之后,如果服务器启动时指定了配置文件,则这里就会开始载入用户给定的配置参数和配置文件redis.conf,并根据用户设定的配置,对server变量相关属性值进行修改。
(3)初始化服务器数据结构(initServer函数)
initServerConfig函数初始化时,程序只创建了命令表一个数据结构,除了这个命令表外,服务器状态还包括其他数据结构需要设置,因此initServer函数的工作如下:
- server.clients:链表初始化,这个链表记录了所有服务器相连的客户端状态结构
- server.slaves:链表初始化,这个链表保存了所有从服务器
- server.monitors:链表初始化,这个链表保存了所有监视器
- server.db:数组初始化,这个数组包含了服务器的所有数据库
- server.el:结构体初始化,这个结构体保存了事件状态,包括文件事件和时间事件,此外还要在这里调用epoll_create函数创建epoll句柄。
typedef struct aeEventLoop {
int maxfd; // 目前已注册的最大描述符
int setsize; // 目前已追踪的最大描述符,默认为server.maxclients+REDIS_EVENTLOOP_FDSET_INCR
long long timeEventNextId; // 用于生成时间事件 id
time_t lastTime; // 最后一次执行时间事件的时间
aeFileEvent *events; // 已注册的文件事件,这个是一个数组,大小为setsize
aeFiredEvent *fired; // 已就绪的文件事件,这个是一个数组,大小为setsize
aeTimeEvent *timeEventHead; // 时间事件这个是一个链表
int stop; // 事件处理器的开关
void *apidata; // I/O多路复用库的私有数据,包括epfd和epoll_event
aeBeforeSleepProc *beforesleep; // 在处理事件前要执行的函数
} aeEventLoop;
- server.pubsub_channels和server.pubsub_patterns:用于保存频道订阅信息和模式订阅信息
- server.lua:保存用于执行Lua脚本的Lua环境
- server.slowlog:用于保存慢查询日志
- 为服务器设置进程信号处理器
- 创建共享对象(createSharedObjects),包括OK、ERR等
- 打开服务器的监听端口(listenToPort,包括创建sockt、绑定地址bind、监听listen)
- 并为监听套接字关联连接应答处理器(aeCreateFileEvent)
- 为serverCron创建时间事件(aeCreateTimeEvent)
- 如果AOF打开,那么打开现有的AOF文件,如果AOF文件不存在,那么创建并打开一个新的AOF文件,为AOF写入做好准备
- 初始化服务器的后台I/O模块,为将来的I/O操作做好准备
(4)还原数据库状态(loadDataFromDisk函数)
完成对server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。
- 如果开启了AOF持久化功能,那么会优先使用AOF文件来恢复数据库。
- 如果没有开启AOF持久化功能,就会使用RDB文件来恢复数据库。
(5)执行事件循环(aeMain函数)
/*
* 事件处理器的主循环
*/
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件处理前执行的函数,那么运行它
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 开始处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
2、文件事件
redis服务器通过套接字和客户端或者其他服务器进行连接,文件事件就是服务器对套接字的抽象,服务器通过监听并处理这些事件来完成一系列网络通信操作。文件事件的结构体如下:
typedef struct aeFileEvent {
// 监听事件类型掩码,
// 值可以是 AE_READABLE 或 AE_WRITABLE ,
// 或者 AE_READABLE | AE_WRITABLE
int mask; /* one of AE_(READABLE|WRITABLE) */
aeFileProc *rfileProc; // 读事件处理器
aeFileProc *wfileProc; // 写事件处理器
void *clientData; // 多路复用库的私有数据
} aeFileEvent;
添加一个文件事件时,需要设置事件循环结构体aeEventLoop中的events数组对应的元素,设置fd对应的文件事件的事件类型和事件处理器,然后把对应的fd注册到epoll。文件事件处理器主要由四个部分组成,分别是:
(1)网络套接字:为每个客户端建立一个socket
(2)I/O多路复用程序:I/O多路复用通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数来实现,redis为每个I/O复用函数都实现了相同的API,程序会在编译时自动选择系统中性能最高的I/O多路复用函数库来作为redis的I/O多路复用程序。
(3)文件事件分派器(dispatcher):接收I/O多路复用传送过来的套接字,并根据套接字产生的事件类型,调用相应的事件处理器。
(4)事件处理器:每个事件处理器是一个函数,他们定义了某个事件发生时,服务器应该执行的操作。redis为文件事件编写了多个处理器,这些处理器分别用于实现不同的网络通信需要:
- 连接应答处理器:acceptTcpHandler对连接服务器监听套接字的客户端进行应答。
- 命令请求处理器:readQueryFromClient从套接字中读取客户端发送的命令请求。
- 命令回复处理器:sendReplyToClent负责将执行命令后得到的命令回复通过套接字返回给客户端。
- 主从同步时的复制处理器:处理主服务器和从服务器的复制操作。
(5)文件事件的API
- aeCreateFileEvent:接收一个套接字描述符、一个事件类型和一个事件处理器作为参数,将给定套接字和给定事件加入到I/O多路复用的监听范围之内。
- aeDeleteFileEvent:接收一个套接字描述符和一个事件类型作为参数,让I/O多路复用程序取消对给定套接字的给定事件的监听。
- aeGetFileEvents:接收一个套接字描述符,返回该套接字正在被监听的事件类型
- asWait;给定一个套接字描述符、一个事件类型和一个毫秒数作为参数,在给定的时间内阻塞并等待套接字的给定事件发生,事件产生或者等待超时后返回
- aeApiPoll:接收一个struct_timeval结构作为参数,在指定时间内阻塞并等待所有被监听的套接字,至少有一个套接字产生,或者等待超时后返回。
- aeProcessEvent:文件事件分析器,先调用aeApiPoll来等待事件的发生,然后遍历所有已产生的事件,调用相应的事件处理器来处理这些事件。
- aeGetApiName:返回I/O多路复用底层使用的I/O多路复用使用的函数库。
3、时间事件
redis服务器中的一些操作,比如serverCron函数需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。时间事件的结构体如下:
typedef struct aeTimeEvent {
long long id; // 时间事件的唯一标识符
// 事件的到达时间
long when_sec; /* seconds */
long when_ms; /* milliseconds */
aeTimeProc *timeProc; // 事件处理函数
aeEventFinalizerProc *finalizerProc; // 事件释放函数
void *clientData; // 多路复用库的私有数据
struct aeTimeEvent *next; // 指向下个时间事件结构,形成链表
} aeTimeEvent;
aeCreateTimeEvent初始化事件事件时,会将serverCron函数指针和时间组成一个aeTimeEvent的结构,然后放入事件循环aeEventLoop的时间事件的链表中。serverCron函数默认每隔100ms执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转。该函数的主要功能如下:
(1)更新服务器时间缓存
redis有不少功能需要获取系统当前时间,而每次获取系统时间都需要进行一次系统调用,为了减少系统调用次数,服务器状态中的unixtime和mstime属性被用作当前时间的缓存。unixtime保存了秒级精度的时间戳,mstime保存了毫秒级精度的时间戳。
(2)更新LRU时钟
服务器状态的lruclock属性保存了服务器的LRU时钟,用于计算键的空转时常。每个redis对象都有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间。当服务器要访问一个键的空转时间时,就会用lruclock减去对象的lru属性记录的时间。
(3)更新服务器每秒执行命令次数
serverCron函数中的trackOperationPerSecond函数会以每100毫秒一次的执行频率,估计并记录服务器最近一秒种处理的命令请求数量。trackOperationPerSecond函数和服务器状态中的四个ops_sec_开头的属性有关,分别是:
- ops_sec_last_sample_time:上一次抽样的时间
- ops_sec_last_sample_ops:上一次抽样时服务器已执行的命令数量
- ops_sec_samples[REDIS_OPS_SEC_SAMPLES]:REDIS_OPS_SEC_SAMPLES为16,数组中每一项都记录了一次抽样结果
- ops_sec_idx:ops_sec_samples数组的索引,每次抽样时增1,到16时归零。
(4)更新服务器内存峰值记录
服务器状态中的stat_peak_memory属性记录了服务器的内存峰值大小,每次serverCrom函数执行都会更新这个值。
(5)处理SIGTERM信号
服务器启动时,redis会为服务器进程的SIGTERM信号关联处理器sigtermHander函数,这个信号负责在服务器收到SIGTERM信号时,打开服务器的shutdown_asap标识。每次serverCrom函数运行时,程序都会对服务器状态的shutdown_asap属性进行检查,并根据属性值决定是否关闭服务器。
(6)管理客户端资源
serverCrom的clientCron函数会对客户端进行检查,如果客户端和服务器的连接已经超时,那么程序释放这个客户端。如果客户端上一次命令执行完了以后,输入缓冲区的大小超过了一定的长度,那么程序就会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存。
(7)管理数据库资源
serverCrom函数中的databaseCron函数会对数据库进行检查,删除其中的过期键,并在有需要时,对字典进行收缩操作。
(8)执行被延迟的BGREWRITEAOF
服务器执行BGSAVE期间,如果客户端发来BGREWRITEAOF命令,那么服务器会将BGREWRITEAOF命令的执行时间延迟到BGSAVE命令之后,每次serverCron函数的执行都会检查。
(9)检查持久化操作的运行状态
服务器使用rdb_child_pid和aof_child_pid属性记录了BGSAVE命令和BGREWRITEAOF的子进程的ID。每次执行serverCron函数时,程序都会检查这两个PID,只要其中的一个不为-1,程序就会执行一次wait3函数,检查子进程是否有信号发来服务器进程:
- 如果有信号到达,说明RDB文件已生成或者AOF文件已生成,服务器需要执行后续操作。
- 如果没有信号到达,则不做任何操作。
如果这两个PID都为-1,表示没有在执行持久化操作,此时,程序需要执行以下三个检查:
- 查看BGREWRITEAOF是否被延迟,被延迟则执行
- 检查服务器的自动保存条件是否满足,满足则调用BGSAVE操作。
- 检查AOF重写条件是否满足,配置文件中auto-aof-rewrite-percentage这个参数设置自动重写的条件,满足则执行一次新的BGREWRITEAOF操作
(10)将AOF缓冲区的内容写入AOF文件中
如果服务器开启了AOF持久化功能,并且AOF缓冲区还有待写入的数据,那么serverCron函数会调用相应的程序,将AOF缓冲区的内容写入到AOF文件里面。
(11)关闭异步客户端
关闭输出缓冲区大小超出限制的客户端。
(12)增加cronloops计数器的值
服务器状态中的cronloops记录了serverCrom函数执行的次数。
(13)时间事件的API:
- aeCreateTimeEvnet:接收一个毫秒数和一个事件事件处理器作为参数,将一个新的时间事件添加到服务器。
- aeDeleteTimeEvent:接收一个时间事件ID作为参数,然后从服务器中删除该ID对应的时间事件
- aeSearchNearestTimer:返回到达时间距离当前时间最接近的那个时间事件
- processTimeEvents:时间事件执行器,这个函数会遍历所有时间事件,并调用事件处理器处理那些已达到的时间事件。
4、事件循环流程
4.1 获取到达事件离当前时间最接近的时间事件
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);
...
}
}
4.2 计算最接近时间事件距离到达还有多少毫秒,保存在timeval中
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
... ...
if (shortest) {
// 如果时间事件存在的话
// 那么根据最近可执行时间事件和现在时间的时间差来决定文件事件的阻塞时间
long now_sec, now_ms;
/* Calculate the time missing for the nearest
* timer to fire. */
// 计算距今最近的时间事件还要多久才能达到
// 并将该时间距保存在 tv 结构中
aeGetTime(&now_sec, &now_ms);
tvp = &tv;
tvp->tv_sec = shortest->when_sec - now_sec;
if (shortest->when_ms < now_ms) {
tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
tvp->tv_sec --;
} else {
tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
}
// 时间差小于 0 ,说明事件已经可以执行了,将秒和毫秒设为 0 (不阻塞)
if (tvp->tv_sec < 0) tvp->tv_sec = 0;
if (tvp->tv_usec < 0) tvp->tv_usec = 0;
} else {
// 执行到这一步,说明没有时间事件
// 那么根据 AE_DONT_WAIT 是否设置来决定是否阻塞,以及阻塞的时间长度
/* 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 */
}
}
... ...
}
4.3 阻塞等待文件事件产生,阻塞时间由timeval决定
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
... ...
// 处理文件事件,阻塞时间由 tvp 决定
numevents = aeApiPoll(eventLoop, tvp);
... ...
}
4.4 处理已产生的文件事件
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
... ...
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 rfired = 0;
/* 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. */
// 读事件
if (fe->mask & mask & AE_READABLE) {
// rfired 确保读/写事件只能执行其中一个
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
// 写事件
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
processed++;
}
... ...
}
4.5 处理已到达的时间事件
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
... ...
/* Check time events */
// 执行时间事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed; /* return the number of processed file/time events */
}