[Redis][12][事件]

第 12 章 事件

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

  • 文件事件:Redis服务器通过socket与客户端连接,而文件事件就是客户端与服务器之间的各种操作,例如:服务器接受并处理客户端请求删除某个键,这就是一个具体的文件事件。
  • 时间事件:Redis服务器的一些操作需要在给定时间点执行,例如前面提到的serverCron函数,就是一种时间事件

下面我们将具体介绍这两种事件,并且说明服务器如何调度这些事件

12.1 文件事件

文件事件交给文件事件处理器来负责。Redis的文件事件处理器基于Reactor模式,并且使用I/O多路服用,这样可以以单线程的方式运行并且监听多个不同的socket

12.1.1 文件事件处理器的构成

文件事件处理器的架构如下图,它包含了4个模块:socket、I/O多路复用程序、文件事件分派器、事件处理器

  • 文件事件是对Socket操作的抽象,每当一个Socket准备好执行连接应答、写入、读取、关闭等操作的时候,就会产生一个文件事件。这些文件事件很可能并发出现
  • I/O多路复用程序负责监听多个Socket端口,并向文件事件分派器传送那些产生了事件的Socket。这时,I/O多路复用会将这些事件都放入一个队列中,这时就把并发变成了有序的、同步的形式。如下图
  • 文件事件分派器接收I/O多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器。
  • 服务器会为执行不同任务的套接字关联不同的事件处理器,这些处理器是一个个函数它们定义了某个事件发生时,服务器应该执行的动作。

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

Redis的I/O多路复用程序的所有功能都是通过包装常见的selectepollexportkqueue这些I/O多路复用函数库来实现的,因为 Redis为每个IO多路复用函数库都实现了相同的API,所以I/O多路复用程序的底层实现是可以互换的,如下图

12.1.3 事件的类型

文件事件其实只有两种类型,AE_READABLE事件和AE_WRITABLE事件

  • 当套接字变得可读时(客户端对套接字执行write操作,或者执行close操作),或者有新的可应答套接字出现时(客户端对服务器的监听套接字执行connect操作),套接字产生AE_READABLE事件。
    当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE事件。

客户端写,则服务器可读。客户端读,则服务器可写。

12.1.4 文件事件的处理器

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

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

当 Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的AE_READABLE事件关联起来,当有客户端连接服务器监听套接字的时候,套接字就会产生AE_READABLE事件,引发连接应答处理器执行,并执行相应的套接字应答操作,如下图

2 命令请求处理器

当一个客户端通过连接应答处理器成功连接到服务器之后,服务器会将客户端套接字的AE_READABLE事件和命令请求处理器关联起来,当客户端向服务器发送命令请求的时候,套接字就会产生AE_READABLE事件,引发命令请求处理器执行,命令请求处理器负责执行命令。

在客户端连接服务器的整个过程中,服务器将会一直为客户端套接字的AE_READABLE事件关联命令请求处理器

3 命令回复处理器

这个处理器负责将服务器执行命令后得到的命令回复通过套接字返回给客户端

当服务器有命令回复需要传送给客户端的时候,服务器会将客户端套接字的AE_WRITABLE事件与命令回复处理器关联起来。

当命令回复发送完毕之后,服务器会解除命令回复处理器与客户端套接字的AE_WRITABLE的关联

4 一次完整的客户端与服务器连接事件示例
  1. 假设一个Redis服务器正在运作,那么这个服务器的监听套接字AE_READABLE事件应该正处于监听状态之下,而该事件所对应的处理器为连接应答处理器
  2. 如果这时有一个Redis客户端向服务器发起连接,那么监听套接字将产生AE_READABLE事件触发连接应答处理器执行。处理器会对客户端的连接请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的AE_READABLE事件与命令请求处理器进行关联,使得客户端可以向主服务器发送命令请求。
  3. 之后,假设客户端向主服务器发送一个命令请求,那么客户端套接字将产生AE_READABLE事件,引发命令请求处理器执行,处理器读取客户端的命令内容,然后通知相关程序去执行。
  4. 执行结束后,当客户端尝试读取命令回复时,就会产生AE_WRITABLE事件,服务器将回复发回给客户端

12.2 时间事件

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

例如下图

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

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

12.3 事件的调度与执行

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

事件的调度和执行由ae.c/aeProcessEvents函数负责,伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def aeProcessEvents():

# 获得到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()

# 计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()

# 若事件已到达,remaind_ms将会是一个负数,这里将负数重设为0
if remaind_ms < 0:
remaind_ms = 0

# 使用remaind_ms创建一个timeval结构体,以传给aeApiPoll函数
timeval = create_timeval_with_ms(remaind_ms)

# 阻塞并等待文件事件的产生
# 这个函数会产生阻塞并监听所有连接,当任何一个连接产生了文件事件,这个方法会返回;此外,若到达了timeval规定的时间,方法也会返回
aeApiPoll(timeval)

# 执行文件事件
processFileEvents()

# 执行时间事件
processTimeEvents()

将aeProcessEvents函数置于一个循环中,再加上初始化和清理函数,就构成了Redis服务器的主函数,伪码如下

1
2
3
4
5
6
7
8
9
def main():

init_server()

# 循环执行aeProcessEvents()以处理各种事件
while server_is_not_shutdown():
aeProcessEvents()

close_server()

整个过程的流程图如下

事件的调度和规则如下

  1. aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这既避免了服务器对时间事件进行频繁的轮询(忙等待),也确保了aeApiPoll函数不会阻塞太长时间
  2. 对文件事件和时间事件的处理是同步、有序、原子地执行
  3. 因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间通常比预设的时间要晚一点