写点什么

让人秒懂的 Redis 的事件处理机制

  • 2022 年 3 月 24 日
  • 本文字数:3299 字

    阅读完需:约 11 分钟

redis 是单进程,单线程模型,与 nginx 的多进程不同,与 golang 的多协程也不同,“工作的工人”那么少,可那么为什么 redis 能这么快呢?

epoll 多路复用

这里重点要说的就是 redis 的 IO 编程模型,首先了解下

为什么要有多路复用呢?

如果没有多路复用,一个线程只能监听一个端口的一个连接,这样这个效率比较低。当然我们有几种办法可以破除这个,一个是使用多线程模型,我们还是监听一个端口,但是一个请求进来,我们为其创建一个线程。但是这种消耗是比较大的。所以我们一直想办法,有没有办法一个线程监听多个端口,或者多个一个端口的多个连接(fd)。

这里再说说 fd, 文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行 I/O 操作(包括网络 socket 操作)的系统调用都通过文件描述符。每个连接请求上来,都会创建一个连接套接字,一个连接使用一个连接套接字。

对于监听端口,我们会有一个监听套接字,对应监听 fd。我们所有的监听业务都是从监听这个套接字开始的。

那么如果我一个程序能同时监听多个连接套接字,是不是就很赞了。是的,这就是 linux 的 io 多路复用逻辑。但是这么多连接套接字,传递数据等是断断续续的,A 连接接收一个包,B 连接再接收一个包,A 连接再接收一个包,B 连接再接收一个包....如果我等着 A 连接把包都接收完再处理 B,那效率是非常慢的。所以,这里我们就需要有一个通知机制,让有收到包的时候通知下处理线程。

linux 的 IO 多路复用逻辑主要有三种:select, poll, epoll。

select

select 模型监听的三个事件:读数据事件,写数据事件,异常事件。

使用 select 模型的步骤如下:

  • 我们确定要监听的监听 fd 列表

  • 调用 select 监听所有监听 fd,阻塞线程。

  • select 只有当有事件出现并且有事件的 fd 已经等待完毕

  • 如果是创建一个连接事件:

  • 创建一个连接套接字,连接 fd

  • 将连接 fd 和监听 fd 集合放在一起

  • 如果是一个读写事件:

  • 遍历所有 fd,判断是否是准备好的 fd

  • 如果是准备好的 fd,进行业务读写逻辑

  • 循环进入 select。

select 一次可以监听 1024 个文件描述符。

poll 模型

poll 传递给内核的是:

  • 监听的 fd 集合

  • 需要监听的事件类型

  • 实际发生的事件类型

poll 的模型逻辑是:

  • 我们确定要监听的监听 fd 列表

  • 调用 poll 监听所有监听 fd,阻塞线程。

  • poll 只有当有事件出现才解除阻塞

  • 如果是创建一个连接事件:

  • 创建一个连接套接字,连接 fd

  • 将连接 fd 和监听 fd 集合放在一起

  • 如果是一个读写事件:

  • 遍历所有 fd,判断是否是有读写事件的 fd

  • 如果 fd 有读写事件,进行业务读写逻辑

  • 循环进入 poll。

poll 比 select 优秀在它没有了 1024 的限制了。但是还是有一些缺陷,就是必须要遍历所有 fd。

现在 C++程序员面临的竞争压力越来越大。那么,作为一名 C++程序员,怎样努力才能快速成长为一名高级的程序员或者架构师,或者说一名优秀的高级工程师或架构师应该有怎样的技术知识体系,这不仅是一个刚刚踏入职场的初级程序员,也是工作三五年之后开始迷茫的老程序员,都必须要面对和想明白的问题。为了帮助大家少走弯路,技术要做到知其然还要知其所以然。以下视频获取点击:C++架构师学习资料

如果想学习 C++工程化、高性能及分布式、深入浅出。性能调优、TCP,协程,Nginx 源码分析 Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,Linux 内核,P2P,K8S,Docker,TCP/IP,协程,DPDK 的朋友可以看一下这个学习地址C/C++Linux服务器开发高级架构师/C++后台架构师​


epoll

epoll 的数据结构类似 poll,但是在调用 epoll 的时候,它不是返回发生了事件的 fd 个数,而是返回了所有发生的事件,这个事件中可以查出发生事件的 fd。

所以 epoll 的逻辑模型是:

  • 我们确定要监听的监听 fd 列表

  • 调用 epoll 监听所有监听 fd,阻塞线程。

  • epoll 只有当有事件出现才解除阻塞,并且返回事件列表

  • 遍历事件列表:

  • 如果是创建一个连接事件:

  • 创建一个连接套接字,连接 fd

  • 将连接 fd 和监听 fd 集合放在一起,继续 epoll

  • 如果是一个读写事件:

  • 处理这个事件

  • 循环进入 epoll。

说白了,epoll 就是我们逻辑上能想到的最优的通知机制。一群人去排队,有多个事件发生,警察来了,那么就告诉警察有哪几个列发生了什么事件,警察一个个处理就行了。

Reactor 模型

有了 IO 多路复用的机制,我们就可以实现一种模型,叫做 Reactor 模型了。Reactor 模型不懂的可以看看这个视频讲解:epoll原理剖析以及reactor模型

最经典的一张图就是这个

​reactor 的五大角色:

  • Handle(句柄或者是描述符)

  • Synchronous Event Demultiplexer(同步事件分离器)

  • Event Handler(事件处理器)

  • Concrete Event Handler(具体事件处理器)

  • Initiation Dispatcher(初始分发器)

简要来说,Reactor 就是我们现在最正常理解的“事件驱动”,对,就是字面理解的那种。比如订阅一个 kafka,我们会创建一个监听程序,监听 kafka 的某个 topic,然后在监听程序中挂载几个处理不同消息的处理程序,每当有一个事件从 topic 进入的时候,我们就会有通过这个监听程序,通知我们的处理程序。处理程序来处理不同的消息。

这种所谓的通知机制,就叫做 reactor。

这个 kafka 的例子,里面有一个监听事件的程序,它一定是一个同步的,一条消息来了,投递一个消息,就叫做 Synchronous Event Demultiplexer(同步事件分离器)。而这个消息,就是 Handle(句柄或者是描述符)。我们需要将某个具体的事件处理函数,也就是上图的 Concrete Event Handler(具体事件处理器) 挂载到监听的处理程序中。当然这里的每个 Concrete Event Handler(具体事件处理器) 都必须遵照某种格式,比如定义了 handle_event 和 get_handle 接口。这种格式我们统称为 Event Handler(事件处理器)。再回到监听事件,监听事件一定有一个挂载的具体 map 之类的结构,即哪个事件对应哪个处理程序,这个挂载的核心我们叫它 Initiation Dispatcher(初始分发器)。

标准的处理流程描述如下:

  • 当应用向 Initiation Dispatcher 注册 Concrete Event Handler 时,应用会标识出该事件处理器希望 Initiation Dispatcher 在某种类型的事件发生发生时向其通知,事件与 handle 关联

  • Initiation Dispatcher 要求注册在其上面的 Concrete Event Handler 传递内部关联的 handle,该 handle 会向操作系统标识

  • 当所有的 Concrete Event Handler 都注册到 Initiation Dispatcher 上后,应用会调用 handle_events 方法来启动 Initiation Dispatcher 的事件循环,这时 Initiation Dispatcher 会将每个 Concrete Event Handler 关联的 handle 合并,并使用 Synchronous Event Demultiplexer 来等待这些 handle 上事件的发生

  • 当与某个事件源对应的 handle 变为 ready 时,Synchronous Event Demultiplexer 便会通知 Initiation Dispatcher。比如 tcp 的 socket 变为 ready for reading

  • Initiation Dispatcher 会触发事件处理器的回调方法。当事件发生时, Initiation Dispatcher 会将被一个“key”(表示一个激活的 handle)定位和分发给特定的 Event Handler 的回调方法

  • Initiation Dispatcher 调用特定的 Concrete Event Handler 的回调方法来响应其关联的 handle 上发生的事件

在这五种角色中,

其中的 Initiation Dispatcher(初始分发器) 是最重要的,我们也称其为 Reactor。它本身定义了一些规范,同时提供了 Handler 的一些注册机制。

而 Synchronous Event Demultiplexer(同步事件分离器) 在 IO 场景下,一般是由操作系统底层实现的,就是说操作系统底层必须能有这个能力,才能基于这个能力实现 Reactor 模型。在我们这个场景下,就是前面提到的 linux 的多路复用机制。

Handle(句柄或者是描述符)在 IO 场景下就是 IO 网络连接的 fd。

而 Event Handler(事件处理器) 和 Concrete Event Handler(具体事件处理器) 在 IO 场景下分为三种处理事件:连接事件,写事件,读事件。对于连接事件的处理器,我们称之为 acceptor,读/写事件的处理器,我们统称为 handler。

所以在 IO 场景下,Reactor 我们需要实现的三个关键角色为:reactor、acceptor、handler。

Redis 的实现

在 redis 中,下面一张图就能说明其实现逻辑。

在 redis 中,有个 reactor(叫做 aeMain)接收客户端的 redis 请求。而在这个 reactor 中除了监听连接事件 acceptor 之外,还可以动态注册各种 handler (aeCreateFileEvent)。当一个客户端请求进入的时候,调用 aeProcessEvents 来分发事件。

这个逻辑就很清晰了吧。整个就是 redis 的事件处理机制。

用户头像

Linux服务器开发qun720209036,欢迎来交流 2020.11.26 加入

专注C/C++ Linux后台服务器开发。

评论

发布
暂无评论
让人秒懂的Redis的事件处理机制_redis_Linux服务器开发_InfoQ写作平台