Redis 核心原理:基于事件的处理流程

2019年04月11日 504点热度 1人点赞 1条评论

本文介绍了 Redis 核心原理和架构:基于事件驱动的模型。事件模型是构成 Redis 内核的引擎,Redis 的丰富功能和组件都是构建在这个模型上的。如果你使用过 Redis,那么本文可以为你打开一道进入 Redis 内部世界的门,窥探 Redis 如何构建它的帝国。
本文先对 Redis 使用的事件模型和原理进行介绍,然后按以下主题顺序展开:

  1. Redis 主程序启动流程
  2. 事件循环(eventloop)
  3. 事件处理器 (event handler)
  4. 事件处理流程

最后以一次客户端 SET 命令操作为例子,讲解一个请求在 Redis 内部的流转是如何完成的。
阅读之前
本文参考的源码基于编写时的最新分支 Redis 5.0.3,实际对照中发现 Redis 的核心逻辑在历史版本迭代中变化不大,也体现了 Redis 的这个核心逻辑的地位。
一、Redis 事件驱动模型
1.1 事件驱动模型

事件驱动,顾名思义,只有在发生某些事件的时候,程序才会有所行动。

事件驱动模型在架构设计领域也称为 Reactor 模式,体现的是一种被动响应的特征。

事件驱动模型通常可以抽象为如下图所示流程:

Redis 核心原理:基于事件的处理流程

主程序处于一个阻塞状态的事件循环(event loop)中等待事件(event),当有事件发生时,根据事件的属性分发到相应的处理函数进行处理。事件以并发的方式发送到服务处理器 (service handler),服务处理器将事件整合到一个有序队列中(这过程称为 demultiplexes),并分发到具体的请求处理器 (request handler)进行处理。
为了阅读的方便,因为「事件」这个词在中文中较常见,所以下文针对事件模型中的「事件」等专用术语,会进行特定的标识,如:事件循环 (event loop),事件 (event),处理器 (handler)等。
1.2 Redis 核心原理
Redis 在事件驱动模型下工作,当有来自外部或内部的请求的时候,才会执行相关的流程。
Redis 程序的整个运作都是围绕事件循环 (event loop)进行的。
事件循环对于 Redis 而言,就像是一台车的引擎一样,提供了整个系统所需的流转动力。所有其他的组件都是基于这个引擎的基础上组合和构建起来的。可以说理解了 Redis 的事件循环就能了解 Redis 的工作原理的核心。
Redis 事件模型如下图所示:

Redis 核心原理:基于事件的处理流程

事件循环 eventloop同时监控多个事件,这里的事件本质上是 Redis 对于连接套接字的抽象。
当套接字变为可读或者可写状态时,就会触发该事件,把就绪的事件放在一个待处理事件的队列中,以有序 (sequentially)、同步 (synchronously) 的方式发送给事件处理器进行处理。这个过程在 Redis 中被称为Fire。
Redis 的事件循环会保存两个列表:events和fired列表,前者表示正在监听的事件,后者表示就绪事件,可以被进一步执行。
在具体实现时,Redis 采用 IO 多路复用 (multiplexing) 的方式,封装了操作系统底层 select/epoll 等函数,实现对多个套接字 (socket) 的监听,这些套接字就是对应多个不同客户端的连接。
最后由对应的处理器将处理的结果返回给客户端去。
Redis事件的来源有两种:文件事件和时间事件,限于篇幅问题,本文主要介绍文件事件的处理流程,时间事件会在文章最后做简要的说明。
以上就概括了Redis 处理用户请求的大致过程。从这个过程我们可以发现:

  1. Redis 处理所有命令都是顺序执行的,其中包括来自客户端的连接请求。所以当 Redis 在处理一个复杂度高、时间很长的请求(比如 KEYS 命令)的时候,其他客户端的连接都没办法相应。
  2. 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相关的功能。
Redis 核心原理:基于事件的处理流程

a. 初始化 Redis 配置
初始化的过程主要做三个事情:

  1. 加载配置
  2. 创建事件循环
  3. 执行事件循环

简化后的代码如下:(跳过不影响理解)

// 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 需要监听某个套接字的时候,就会创建一个事件,并注册到事件循环中进行监听,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()中具体做了几个事情:

  1. 调用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 接受一个客户端操作的完整流程的介绍。如果对初始化过程还有问题,请参考上文。
本章主要分为两个阶段:

  1. 第一个阶段:一个外部客户端与 Redis 服务器建立 TCP 连接。
  2. 比如我们常用的 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 核心原理:基于事件的处理流程

当有客户端连接到 Redis 服务器的时候,注册在事件循环中的监听服务端口的事件就会变成读就绪状态,从而触发这个事件到待处理事件队列中,准备调用acceptTcpHandler进行处理。

  1. 为在服务器端创建一个对应本次连接的套接字。
  2. 把服务端套接字的文件描述符cfd作为参数,创建client变量。
  3. 为该客户端连接创建并注册一个关联了readQueryFromClient处理器的可读事件到事件循环,用于下一步接收并执行命令的工作。

3.2 一次客户端连接和调用命令的执行流程
如图展示一个客户端已经完成了连接,对 Redis 服务器发起一次SET操作后,Redis 处理命令的完整流程。

Redis 核心原理:基于事件的处理流程

在上一节中提到,当一个客户端建立连接后,会有一个可读事件关联到事件循环,等待接收命令。当有客户端发起一次命令操作后,Redis 就会调用readQueryFromClient处理器,对用户发送过来的请求,按 RESP (REdis Serialization Protocol) 进行解析处理后,调用相关的命令进行处理。

  1. 调用命令的函数主要做两个事情:(1)查找对应的命令,比如这里的SET(2)调用该命令关联的函数进行处理,这里就是setCommand。
  2. setCommand函数将客户端传进来的参数,变更数据库对应 KEY 的值,然后回复客户端。
  3. 回复客户端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 也是核心流程中重要的一个组成部分,限于篇幅不在这里详细介绍。但有了对事件循环的认识,要理解时间事件本身也不会太困难。

Horry

一个专门收集Android面试题的网站

文章评论

您需要 登录 之后才可以评论