本文介绍了 redis 核心原理和架构:基于事件驱动的模型。事件模型是构成 Redis 内核的引擎,Redis 的丰富功能和组件都是构建在这个模型上的。如果你使用过 Redis,那么本文可以为你打开一道进入 Redis 内部世界的门,窥探 Redis 如何构建它的帝国。
本文先对 Redis 使用的事件模型和原理进行介绍,然后按以下主题顺序展开:
最后以一次客户端 SET 命令操作为例子,讲解一个请求在 Redis 内部的流转是如何完成的。
阅读之前
为了方便公众号上进行阅读,帮助读者快速掌握 Redis 核心原理,本文对 Redis 模型进行了简化,去掉了大量的检查和异常处理流程,并且仅在必要的时候通过代码说明。
本文参考的源码基于编写时的最新分支 Redis 5.0.3,实际对照中发现 Redis 的核心逻辑在历史版本迭代中变化不大,也体现了 Redis 的这个核心逻辑的地位。
一、Redis 事件驱动模型
1.1 事件驱动模型
事件驱动,顾名思义,只有在发生某些事件的时候,程序才会有所行动。
事件驱动模型在架构设计领域也称为 Reactor 模式,体现的是一种被动响应的特征。
事件驱动模型通常可以抽象为如下图所示流程:
主程序处于一个阻塞状态的事件循环(event loop)中等待事件(event),当有事件发生时,根据事件的属性分发到相应的处理函数进行处理。事件以并发的方式发送到服务处理器 (service handler),服务处理器将事件整合到一个有序队列中(这过程称为 demultiplexes),并分发到具体的请求处理器 (request handler)进行处理。
为了阅读的方便,因为「事件」这个词在中文中较常见,所以下文针对事件模型中的「事件」等专用术语,会进行特定的标识,如:事件循环 (event loop),事件 (event),处理器 (handler)等。
1.2 Redis 核心原理
Redis 在事件驱动模型下工作,当有来自外部或内部的请求的时候,才会执行相关的流程。
Redis 程序的整个运作都是围绕事件循环 (event loop)进行的。
事件循环对于 Redis 而言,就像是一台车的引擎一样,提供了整个系统所需的流转动力。所有其他的组件都是基于这个引擎的基础上组合和构建起来的。可以说理解了 Redis 的事件循环就能了解 Redis 的工作原理的核心。
Redis 事件模型如下图所示:
事件循环 eventloop同时监控多个事件,这里的事件本质上是 Redis 对于连接套接字的抽象。
当套接字变为可读或者可写状态时,就会触发该事件,把就绪的事件放在一个待处理事件的队列中,以有序 (sequentially)、同步 (synchronously) 的方式发送给事件处理器进行处理。这个过程在 Redis 中被称为Fire。
Redis 的事件循环会保存两个列表:events和fired列表,前者表示正在监听的事件,后者表示就绪事件,可以被进一步执行。
在具体实现时,Redis 采用 IO 多路复用 (multiplexing) 的方式,封装了操作系统底层 select/epoll 等函数,实现对多个套接字 (socket) 的监听,这些套接字就是对应多个不同客户端的连接。
最后由对应的处理器将处理的结果返回给客户端去。
Redis事件的来源有两种:文件事件和时间事件,限于篇幅问题,本文主要介绍文件事件的处理流程,时间事件会在文章最后做简要的说明。
以上就概括了Redis 处理用户请求的大致过程。从这个过程我们可以发现:
1.3 事件驱动模型的优势
有利于架构解耦和模块化开发
有利于功能架构实现上更加解耦,模块的可重用性更高。因事件循环的流程本身和具体的处理逻辑之间是独立的,只要在创建事件的时候关联特定的处理逻辑(事件处理器),就可以完成一次事件的创建和处理。
有利于减小高并发量情况下对性能的影响
根据论文 SEDA: An Architecture for Well-Conditioned, Scalable Internet Services 的测试结果显示,相比一个连接分配一个线程的模型, Reactor 模式(固定线程数)在连接数增大的情况下吞吐量不会明显降低,延时也不会也受到显著的影响。
二、事件循环的 Redis 实现
下面开始,会对 Redis 如何实现事件循环进行说明,会涉及到一些源码的实现部分,如果不感兴趣可以直接跳到第三节看 Redis 怎么利用事件处理模型来处理具体的命令。
2.1 Redis 事件循环 Event Loop
Redis 的事件循环,最直观的理解,就是一个在不断等待事件的一个无限循环,直到 Redis 程序退出。
Redis 实现事件循环主要涉及三个源码文件:server.c, ae.c, networking.c。
a. 初始化 Redis 配置
初始化的过程主要做三个事情:
简化后的代码如下:(跳过不影响理解)
// 0. 定义服务器主要结构体, 加载服务器配置 struct redisServer server; initServerConfig(); loadServerConfig(); // 1. 根据配置参数初始化, initServer() { // 1.1 实际创建事件循环 server.el = aeCreateEventLoop(); // 1.2 为事件循环注册一个可读事件,用于响应外部客户端请求 aeCreateFileEvent(server.el, AE_READABLE, acceptTcpHandler) } // 2. 执行事件循环,等待连接和命令请求 aeMain(server.el);
初始化过程中被创建的server.el包含了两个事件的列表,它的结构体实现如下:
typedef struct aeEventLoop { aeFileEvent events[AE_SETSIZE]; /* 注册的事件,被 eventloop 监听 */ aeFiredEvent fired[AE_SETSIZE]; /* 有读写操作需要执行的事件(就绪事件) */ } aeEventLoop;
b. 创建事件循环
主循环体aeMain()在ae.c文件中被实现,简化后的代码如下:
void aeMain(aeEventLoop *eventLoop) { while (!eventLoop->stop) { aeProcessEvents(eventLoop, AE_ALL_EVENTS); } }
事件循环主要就是一个while循环,不断去轮询是否有就绪的事件需要处理,具体的处理函数是aeProcessEvents,接下来会有对这个函数有更详细的介绍。
c. 创建用于监听端口的事件
在上述 Redis 在初始化时,程序会创建一个关联了acceptTcpHandler处理器的可读事件:
aeCreateFileEvent(server.el, AE_READABLE, acceptTcpHandler)
这个可读事件注册到事件循环中,就实现了 Redis 对外提供的服务地址和端口的连接服务。具体的内容下一个小节事件处理器中介绍。
2.2 事件处理器 Event Handler
所有事件被创建时,都会关联一个处理器 (handler),并注册到事件循环中,事件处理器用于具体的读写操作。
Redis 的常用几个事件处理器有:
以上处理器均在networking.c文件下实现,该文件负责 Redis 所有网络 IO 功能的实现。
一个客户端一次正常的连接和命令操作流程,可以通过上述三个处理器完成。
当 Redis 需要监听某个套接字的时候,就会创建一个事件,并注册到事件循环中进行监听,Redis 将处理器以参数的方式关联到事件中。
比如以下是注册一个可读事件的操作:
aeCreateFileEvent(server.el, fd, AE_READABLE, readQueryFromClient, c)
注册完毕后,事件循环就会将这个事件(套接字)加入到监听的范围,当事件可读时,Redis 就会将这个事件发送到待处理事件队列中等待处理,等到可读就绪时,会被readQueryFromClient处理器处理。
可以看到整个过程中事件循环和不同处理器之间是解耦的,互不干扰。这样实现提高了代码的简洁和重用。
2.3 事件处理 Process Events
在 Redis 完成初始化、创建事件循环后,就会处于等待和处理事件的状态:无限循环aeProcessEvents()函数。
这个函数在ae.c中实现,该文件主要负责事件循环的实现,在aeProcessEvents()中具体做了几个事情:
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) { aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd]; fe->rfileProc() // 读事件处理 fe->wfileProc() // 写事件处理 }
fe就是要处理的文件事件 file event,对应读操作或写操作。至于处理的具体操作,则由创建事件时自身关联的处理器决定的,事件循环不需要关注。
processTimeEvents(eventLoop);
至此,Redis 的事件循环的机制已经介绍完毕,可以观察到整个事件循环的逻辑过程都没有涉及具体的命令操作,只需要定义事件的类型和处理器即可。可以说这部分就是Reactor 模式体现出来的一个好处:接收事件和处理流程的实现相互解耦。
三、一次命令操作的完整流程
本章是建立在 Redis 已经完成了初始化工作,主要是创建事件循环之后,Redis 接受一个客户端操作的完整流程的介绍。如果对初始化过程还有问题,请参考上文。
本章主要分为两个阶段:
➜ ~ telnet 127.0.0.1 6379 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'.
set a 1 +OK
3.1 一个客户端连接进服务器的过程
如图,展示一个新的外部客户端与 Redis 服务器建立连接的过程。
当有客户端连接到 Redis 服务器的时候,注册在事件循环中的监听服务端口的事件就会变成读就绪状态,从而触发这个事件到待处理事件队列中,准备调用acceptTcpHandler进行处理。
3.2 一次客户端连接和调用命令的执行流程
如图展示一个客户端已经完成了连接,对 Redis 服务器发起一次SET操作后,Redis 处理命令的完整流程。
在上一节中提到,当一个客户端建立连接后,会有一个可读事件关联到事件循环,等待接收命令。当有客户端发起一次命令操作后,Redis 就会调用readQueryFromClient处理器,对用户发送过来的请求,按 RESP (REdis Serialization Protocol) 进行解析处理后,调用相关的命令进行处理。
返回结果给客户端
以上是整个SET命令的事件处理,不过在这个时候,返回给用户的回复内容,只存放于服务器的客户端变量输出缓冲中。至于将结果返回给用户的过程,取决于版本,有不同的操作。
在 4.0 以前,每次的addReply操作会创建一个写事件,然后放到事件循环中执行。
而 4.0 开始,在每次重新进入一个新的循环之前,就是eventLoop->beforesleep();这个操作,Redis 会尝试直接发送给客户端,只有当发送的内容超过一定大小,无法一次发送完成的时候,才会去创建一个可写事件。
有兴趣的读者可以去看下 Redis 作者的这个 commit:
antirez in commit 1c7d87d: Avoid installing the client write handler when possible.
目的是减少一次系统调用,适用于大部分操作类命令的回复。
可以观察到,整个操作的实现过程,和事件循环本身没有交集的(没有涉及到ae.c),开发者只需要关心具体命令的处理逻辑即可。
四、补充说明