本文介绍了 Redis 核心原理和架构:基于事件驱动的模型。事件模型是构成 Redis 内核的引擎,Redis 的丰富功能和组件都是构建在这个模型上的。如果你使用过 Redis,那么本文可以为你打开一道进入 Redis 内部世界的门,窥探 Redis 如何构建它的帝国。
本文先对 Redis 使用的事件模型和原理进行介绍,然后按以下主题顺序展开:
- Redis 主程序启动流程
- 事件循环(eventloop)
- 事件处理器 (event handler)
- 事件处理流程
最后以一次客户端 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 处理用户请求的大致过程。从这个过程我们可以发现:
- Redis 处理所有命令都是顺序执行的,其中包括来自客户端的连接请求。所以当 Redis 在处理一个复杂度高、时间很长的请求(比如 KEYS 命令)的时候,其他客户端的连接都没办法相应。
- Redis 内部定时执行的任务也是放在顺序队列中处理,其中也可能包含时间较长的任务,比如自动删除一个过期的大 Key,比如很大 list, hash, set 等。所以有时候会遇到明明业务没有主动操作复杂,但也会出现卡顿的问题。
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。
- server.c 的 main()函数是整个 Redis 程序的开始,我们也从这里开始观察 Redis 的行为。
- ae.c实现事件循环和事件的相关功能。
- networking.c则负责处理网络IO相关的功能。
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 的常用几个事件处理器有:
- 响应连接的处理器acceptTcpHandler()
- 读取客户端命令的处理器readQueryFromClient()
- 返回处理结果的处理器sendReplyToClient()
以上处理器均在networking.c文件下实现,该文件负责 Redis 所有网络 IO 功能的实现。
一个客户端一次正常的连接和命令操作流程,可以通过上述三个处理器完成。
当 Redis 需要监听某个套接字的时候,就会创建一个事件,并注册到事件循环中进行监听,Redis 将处理器以参数的方式关联到事件中。
比如以下是注册一个可读事件的操作:
aeCreateFileEvent(server.el, fd, AE_READABLE, readQueryFromClient, c)
- server.el:事件循环 eventloop,一个服务器只有一个el
- fd:表示这个客户端连接的文件描述符,每个客户端连接对应一个
- AE_READABLE:表示这是一个可读事件,可以理解为客户端准备进行写操作
- readQueryFromClient: 这个事件关联的处理器,当事件就绪后,就会调用此处理器
- c:表示这个客户端在Redis中指向的变量
注册完毕后,事件循环就会将这个事件(套接字)加入到监听的范围,当事件可读时,Redis 就会将这个事件发送到待处理事件队列中等待处理,等到可读就绪时,会被readQueryFromClient处理器处理。
可以看到整个过程中事件循环和不同处理器之间是解耦的,互不干扰。这样实现提高了代码的简洁和重用。
2.3 事件处理 Process Events
在 Redis 完成初始化、创建事件循环后,就会处于等待和处理事件的状态:无限循环aeProcessEvents()函数。
这个函数在ae.c中实现,该文件主要负责事件循环的实现,在aeProcessEvents()中具体做了几个事情:
- 调用IO多路复用函数(select, epoll, evport, kqueue中的其中一种),阻塞等待事件变成就绪状态或者直到超时,如果有事件就绪,就会将相应事件加入到eventLoop的待处理事件队列 eventLoop->fired 中,然后进入下一个循环。
numevents = aeApiPoll(eventLoop, tvp);
- 如果在上一步中,发现有numevents个事件被触发,就会将就绪队列的事件一个个按顺序进行处理,处理的函数为
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 接受一个客户端操作的完整流程的介绍。如果对初始化过程还有问题,请参考上文。
本章主要分为两个阶段:
- 第一个阶段:一个外部客户端与 Redis 服务器建立 TCP 连接。
- 比如我们常用的 Telnet 到 Redis 端口的操作。
➜ ~ telnet 127.0.0.1 6379 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'.
- 第二阶段:已经建立连接的客户端,对Redis 发起一次SET命令的操作。
set a 1 +OK
3.1 一个客户端连接进服务器的过程
如图,展示一个新的外部客户端与 Redis 服务器建立连接的过程。
当有客户端连接到 Redis 服务器的时候,注册在事件循环中的监听服务端口的事件就会变成读就绪状态,从而触发这个事件到待处理事件队列中,准备调用acceptTcpHandler进行处理。
- 为在服务器端创建一个对应本次连接的套接字。
- 把服务端套接字的文件描述符cfd作为参数,创建client变量。
- 为该客户端连接创建并注册一个关联了readQueryFromClient处理器的可读事件到事件循环,用于下一步接收并执行命令的工作。
3.2 一次客户端连接和调用命令的执行流程
如图展示一个客户端已经完成了连接,对 Redis 服务器发起一次SET操作后,Redis 处理命令的完整流程。
在上一节中提到,当一个客户端建立连接后,会有一个可读事件关联到事件循环,等待接收命令。当有客户端发起一次命令操作后,Redis 就会调用readQueryFromClient处理器,对用户发送过来的请求,按 RESP (REdis Serialization Protocol) 进行解析处理后,调用相关的命令进行处理。
- 调用命令的函数主要做两个事情:(1)查找对应的命令,比如这里的SET(2)调用该命令关联的函数进行处理,这里就是setCommand。
- setCommand函数将客户端传进来的参数,变更数据库对应 KEY 的值,然后回复客户端。
- 回复客户端addReply函数将返回给客户端的内容,写到客户端变量的输出缓冲client.buf中,等待发送给客户端。
返回结果给客户端
以上是整个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),开发者只需要关心具体命令的处理逻辑即可。
四、补充说明
- 事件都是来源于外部客户端吗?
- 这要看怎么定义“外部客户端”了。首先事件本身分为两种大类:文件事件和时间事件。本文主要介绍文件事件。而文件事件的产生可以是来源于网络客户端的连接,正如本文所描述的,也可以来自 Redis 集群内部运行需要,会使用一些伪客户端来触发一些文件事件。
- 举个例子,当有从节点 (slave/replica) 向主节点 (master) 发起一次同步的时候,在 Redis 就会产生一个需要处理同步数据的事件。不过严格意义上来讲,这个从节点对于主节点 Redis 来说,也属于“外部客户端”。正常情况下,Redis 自身不会主动产生文件事件。
- Redis 是怎么定期更新状态、删除过期KEY的?
- 读者大概猜到我要引出时间事件这个概念了。Redis 会定期执行服务器的检查,以及一些周期操作,这个周期由参数hz决定,默认情况下是100毫秒触发一次检查,执行该周期内的时间事件。
- 时间事件 是 Redis 也是核心流程中重要的一个组成部分,限于篇幅不在这里详细介绍。但有了对事件循环的认识,要理解时间事件本身也不会太困难。
为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜! 同时小编也整理了一套学习资料,可加小编的Q群 902570485 获取资料(点击群号即可立即进群领取)合理利用自己每一分每一秒的时间来学习提升自己。