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

Redis 核心原理:基于事件的处理流程_第1张图片
笔者:单大
有任何疑问欢迎关注微信公众号:网易游戏运维平台。(长按识别上图二维码)
微信公众号原文链接:Redis 核心原理:基于事件的处理流程

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

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

本文先对 Redis 使用的事件模型和原理进行介绍,然后按以下主题顺序展开:

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

最后以一次客户端 SET 命令操作为例子,讲解一个请求在 Redis 内部的流转是如何完成的。

阅读之前

为了方便公众号上进行阅读,帮助读者快速掌握 Redis 核心原理,本文对 Redis 模型进行了简化,去掉了大量的检查和异常处理流程,并且仅在必要的时候通过代码说明。

本文参考的源码基于编写时的最新分支 Redis 5.0.3,实际对照中发现 Redis 的核心逻辑在历史版本迭代中变化不大,也体现了 Redis 的这个核心逻辑的地位。

文章目录

  • Redis 核心原理:基于事件的处理流程
        • 阅读之前
    • 一、Redis 事件驱动模型
      • 1.1 事件驱动模型
      • 1.2 Redis 核心原理
      • 1.3 事件驱动模型的优势
        • 有利于架构解耦和模块化开发
        • 有利于减小高并发量情况下对性能的影响
    • 二、事件循环的 Redis 实现
      • 2.1 Redis 事件循环 Event Loop
        • a. 初始化 Redis 配置
        • b. 创建事件循环
        • c. 创建用于监听端口的事件
      • 2.2 事件处理器 Event Handler
      • 2.3 事件处理 Process Events
    • 三、一次命令操作的完整流程
      • 3.1 一个客户端连接进服务器的过程
    • 3.2 一次客户端连接和调用命令的执行流程
        • 返回结果给客户端
    • 四、补充说明
    • 五、参考

一、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 的事件循环会保存两个列表:eventsfired列表,前者表示正在监听的事件,后者表示就绪事件,可以被进一步执行。

在具体实现时,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.cmain()函数是整个 Redis 程序的开始,我们也从这里开始观察 Redis 的行为。
  • ae.c实现事件循环和事件的相关功能。
  • networking.c则负责处理网络IO相关的功能。

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 将处理器以参数的方式关联到事件中。

比如以下是注册一个可读事件的操作:

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); 
    
  2. 如果在上一步中,发现有numevents事件被触发,就会将就绪队列的事件一个个按顺序进行处理,处理的函数为

    for (j = 0; j < numevents; j++) {
        aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
    	fe->rfileProc() // 读事件处理
    	fe->wfileProc() // 写事件处理
    }
    

    fe就是要处理的文件事件 file event,对应读操作或写操作。至于处理的具体操作,则由创建事件时自身关联的处理器决定的,事件循环不需要关注。

  3. 最后一步:如果有时间事件,则进行时间事件的处理:

    processTimeEvents(eventLoop);
    

至此,Redis 的事件循环的机制已经介绍完毕,可以观察到整个事件循环的逻辑过程都没有涉及具体的命令操作,只需要定义事件的类型和处理器即可。可以说这部分就是Reactor 模式体现出来的一个好处:接收事件和处理流程的实现相互解耦。

三、一次命令操作的完整流程

本章是建立在 Redis 已经完成了初始化工作,主要是创建事件循环之后,Redis 接受一个客户端操作的完整流程的介绍。如果对初始化过程还有问题,请参考上文。

本章主要分为两个阶段:

  1. 第一个阶段:一个外部客户端与 Redis 服务器建立 TCP 连接。
    比如我们常用的 Telnet 到 Redis 端口的操作。

    ➜  ~ telnet 127.0.0.1 6379
    Trying 127.0.0.1...
    Connected to 127.0.0.1.
    Escape character is '^]'.
    
  2. 第二阶段:已经建立连接的客户端,对Redis 发起一次SET命令的操作。

    set a 1
    +OK
    

3.1 一个客户端连接进服务器的过程

如图,展示一个新的外部客户端与 Redis 服务器建立连接的过程。

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

  2. 为在服务器端创建一个对应本次连接的套接字。

  3. 把服务端套接字的文件描述符cfd作为参数,创建client变量。

  4. 为该客户端连接创建并注册一个关联了readQueryFromClient处理器的可读事件到事件循环,用于下一步接收并执行命令的工作。

3.2 一次客户端连接和调用命令的执行流程

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

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

  2. 调用命令的函数主要做两个事情:(1)查找对应的命令,比如这里的SET(2)调用该命令关联的函数进行处理,这里就是setCommand

  3. setCommand函数将客户端传进来的参数,变更数据库对应 KEY 的值,然后回复客户端。

  4. 回复客户端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 也是核心流程中重要的一个组成部分,限于篇幅不在这里详细介绍。但有了对事件循环的认识,要理解时间事件本身也不会太困难。

五、参考

Redis 源码:是本文主要内容参考。

Redis 官方 README.md:概述核心代码的结构和用途。

Redis Event Library:关于 Redis 的 Event Library 工作原理的官方说明文档。

你可能感兴趣的:(Redis 核心原理:基于事件的处理流程)