更多内容,前往个人博客
一、概述
【1】Redis
是基于 Reactor
模式开发的网络事件处理器:这个处理器被称为文件事件处理器(file event handler
),这个文件事件处理器是单线程的,所以 Redis
才叫做单线程的模型:
■ 文件事件处理器使用 I/O
多路复用(multiplexing)机制监听多个套接字 Socket
,根据 Socket
上的事件来选择对应的事件处理器进行处理。
■ 当被监听的套接字准备好执行连接应答(accept
)、读取(read
)、写入(write
)、关闭(close
)等操作时。与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
【2】虽然文件事件处理器以单线程的方式运行,但其使用 I/O
多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis
服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis
内部单线程设计的简单性。
二、文件事件处理器的结构
【1】文件事件处理器的结构包含 4 个部分:
● 多个 socket
● IO
多路复用程序
● 文件事件分派器
● 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
【2】多个 socket
可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO
多路复用程序会监听多个 socket
,会将 socket
产生的事件放入队列中排队,以有序(sequentially
)、同步(synchronously
)、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕), I/O
多路复用程序才会继续向文件事件分派器传送下一个套接字, 如图:
文件事件分派器接收 I/O
多路复用程序传来的套接字, 并根据套接字产生的事件的类型, 调用相应的事件处理器。服务器会为执行不同任务的套接字关联不同的事件处理器, 这些处理器是一个个函数, 它们定义了某个事件发生时, 服务器应该执行的动作。
【3】I/O
多路复用程序的实现: Redis
的 I/O
多路复用程序的所有功能都是通过包装常见的 select
、epoll
、evport
和 kqueue
这些 I/O
多路复用函数库来实现的, 每个 I/O
多路复用函数库在 Redis 源码中都对应一个单独的文件, 比如 ae_select.c、ae_epoll.c、ae_kqueue.c
, 诸如此类。因为 Redis
为每个 I/O
多路复用函数库都实现了相同的 API
, 所以 I/O
多路复用程序的底层实现是可以互换的, 如下图所示:
Redis
在 I/O
多路复用程序的实现源码中用 #include
宏定义了相应的规则, 程序会在编译时自动选择系统中性能最高的 I/O
多路复用函数库来作为 Redis
的 I/O
多路复用程序的底层实现:
1 /* 包括此系统支持的最佳复用层。2 * 以下应按性能降序排列。 */3 #ifdef HAVE_EVPORT4 #include "ae_evport.c"5 #else6 #ifdef HAVE_EPOLL7 #include "ae_epoll.c"8 #else9 #ifdef HAVE_KQUEUE
10 #include "ae_kqueue.c"
11 #else
12 #include "ae_select.c"
13 #endif
14 #endif
15 #endif
【4】事件的类型:I/O
多路复用程序可以监听多个套接字的 ae.h/AE_READABLE
事件和 ae.h/AE_WRITABLE
事件, 这两类事件和套接字操作之间的对应关系如下:
■ 当套接字变得可读时(客户端对套接字执行 write
操作,或者执行 close
操作), 或者有新的可应答(acceptable
)套接字出现时(客户端对服务器的监听套接字执行 connect
操作), 套接字产生 AE_READABLE
事件。
■ 当套接字变得可写时(客户端对套接字执行 read
操作), 套接字产生 AE_WRITABLE
事件。
I/O
多路复用程序允许服务器同时监听套接字的AE_READABLE
事件和AE_WRITABLE
事件, 如果一个套接字同时产生了这两种事件, 那么文件事件分派器会优先处理AE_READABLE
事件, 等到AE_READABLE
事件处理完之后, 才处理AE_WRITABLE
事件。这也就是说, 如果一个套接字又可读又可写的话, 那么服务器将先读套接字, 后写套接字。
【5】API
:ae.c/aeCreateFileEvent
函数接收一个套接字描述符、 一个事件类型、 以及一个事件处理器作为参数, 将给定套接字的给定事件加入到 I/O
多路复用程序的监听范围之内, 并对事件和事件处理器进行关联。ae.c/aeDeleteFileEvent
函数接收一个套接字描述符和一个监听事件类型作为参数, 让 I/O
多路复用程序取消对给定套接字的给定事件的监听, 并取消事件和事件处理器之间的关联。ae.c/aeGetFileEvents
函数接收一个套接字描述符, 返回该套接字正在被监听的事件类型:
■ 如果套接字没有任何事件被监听, 那么函数返回 AE_NONE
;
■ 如果套接字的读事件正在被监听, 那么函数返回 AE_READABLE
;
■ 如果套接字的写事件正在被监听, 那么函数返回 AE_WRITABLE
;
■ 如果套接字的读事件和写事件正在被监听, 那么函数返回 AE_READABLE | AE_WRITABLE
;
ae.c/aeWait
函数接受一个套接字描述符、一个事件类型和一个毫秒数为参数, 在给定的时间内阻塞并等待套接字的给定类型事件产生, 当事件成功产生, 或者等待超时之后, 函数返回。
ae.c/aeApiPoll
函数接受一个 sys/time.h/struct timeval
结构为参数, 并在指定的时间內, 阻塞并等待所有被 aeCreateFileEvent
函数设置为监听状态的套接字产生文件事件, 当有至少一个事件产生, 或者等待超时后, 函数返回。
ae.c/aeProcessEvents
函数是文件事件分派器, 它先调用 aeApiPoll
函数来等待事件产生, 然后遍历所有已产生的事件, 并调用相应的事件处理器来处理这些事件。
ae.c/aeGetApiName
函数返回 I/O
多路复用程序底层所使用的 I/O
多路复用函数库的名称: 返回 "epoll"
表示底层为 epoll
函数库, 返回"select"
表示底层为 select
函数库, 诸如此类。
【6】文件事件的处理器:Redis
为文件事件编写了多个处理器, 这些事件处理器分别用于实现不同的网络通讯需求, 比如:
■ 为了对连接服务器的各个客户端进行应答, 服务器要为监听套接字关联连接应答处理器;
■ 为了接收客户端传来的命令请求, 服务器要为客户端套接字关联命令请求处理器;
■ 为了向客户端返回命令的执行结果, 服务器要为客户端套接字关联命令回复处理器;
■ 当主服务器和从服务器进行复制操作时, 主从服务器都需要关联特别为复制功能编写的复制处理器;
在这些事件处理器里面, 服务器最常用的要数与客户端进行通信的连接应答处理器、 命令请求处理器和命令回复处理器。
【7】连接应答处理器:networking.c/acceptTcpHandler
函数是 Redis
的连接应答处理器, 这个处理器用于对连接服务器监听套接字的客户端进行应答, 具体实现为sys/socket.h/accept
函数的包装。当 Redis
服务器进行初始化的时候, 程序会将这个连接应答处理器和服务器监听套接字的 AE_READABLE
事件关联起来, 当有客户端用sys/socket.h/connect
函数连接服务器监听套接字的时候, 套接字就会产生 AE_READABLE
事件, 引发连接应答处理器执行, 并执行相应的套接字应答操作, 如图 IMAGE_SERVER_ACCEPT_CONNECT
所示。
【8】命令请求处理器:networking.c/readQueryFromClient
函数是 Redis 的命令请求处理器, 这个处理器负责从套接字中读入客户端发送的命令请求内容, 具体实现为 unistd.h/read
函数的包装。当一个客户端通过连接应答处理器成功连接到服务器之后, 服务器会将客户端套接字的 AE_READABLE
事件和命令请求处理器关联起来, 当客户端向服务器发送命令请求的时候, 套接字就会产生 AE_READABLE
事件, 引发命令请求处理器执行, 并执行相应的套接字读入操作, 如图 IMAGE_SERVER_RECIVE_COMMAND_REQUEST
所示。
当命令回复发送完毕之后, 服务器就会解除命令回复处理器与客户端套接字的 AE_WRITABLE
事件之间的关联。
三、客户端与 redis
的一次通信过程
【1】客户端 socket01
向 redis
的 server
socket
请求建立连接,此时 server
socket
会产生一个 AE_READABLE
事件,IO
多路复用程序监听到 server
socket
产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01
,并将该 socket01
的 AE_READABLE
事件与命令请求处理器关联。
【2】假设此时客户端发送了一个 set key value
请求,此时 redis
中的 socket01
会产生 AE_READABLE
事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 socket01
的 AE_READABLE
事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01
的 key value
并在自己内存中完成 key value
的设置。操作完成后,它会将 socket01
的 AE_WRITABLE
事件与命令回复处理器关联。
【3】如果此时客户端准备好接收返回结果了,那么 redis
中的 socket01
会产生一个 AE_WRITABLE
事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01
输入本次操作的一个结果,比如 ok
,之后解除 socket01
的 AE_WRITABLE
事件与命令回复处理器的关联。这样便完成了一次通信。
四、为啥 redis 单线程模型也能效率这么高
■ 纯内存操作
■ 核心是基于非阻塞的 IO
多路复用机制
■ 单线程反而避免了多线程的频繁上下文切换问题