查看原文
其他

理解Redis的反应堆模式

后端技术指南针 后端技术指南针 2021-02-01

1. Redis的网络模型

Redis基于Reactor模式(反应堆模式)开发了自己的网络模型,形成了一个完备的基于IO复用的事件驱动服务器,但是不由得浮现几个问题:  
  •  为什么要使用Reactor模式呢?

  •  Redis如何实现自己的Reactor模式?


2. Reactor模式的背景

单纯的epoll/kqueue可以单机支持数万并发,单纯从性能的角度而言毫无问题,但是技术实现和软件设计仍然存在一些差异。
设想这样一种场景:
  • epoll/kqueue将收集到的可读写事件全部放入队列中等待业务线程的处理,此时线程池的工作线程拿到任务进行处理,实际场景中可能有很多种请求类型,工作线程每拿到一种任务就进行相应的处理,处理完成之后继续处理其他类型的任务;

  • 工作线程需要关注各种不同类型的请求,对于不同的请求选择不同的处理方法,因此请求类型的增加会让工作线程复杂度增加,维护起来也变得越来越困难;


作为吃货的你,如果还是没听懂,不用着急,想象一下吃饭的场景:
城南大熊饭店在来客高峰期服务员快速地接待了很多顾客,有的顾客点凉菜,有的点热菜,有的点主食,有的点饮料等等,在后厨如果人员没有分工,那么厨师A一会儿弄凉菜一会弄主食,厨师B一会儿弄热菜一会儿弄饮料,厨师C一会儿弄主食一会儿弄火锅.....虽然最终也响应了顾客的需求,但是把这帮厨师累够呛并且厨师都是全栈厨师(FullStack),对饭店来说招个这样的厨师非常不容易而且薪水也高,老板孙大熊很苦恼。
当局者迷旁观者清,孙大熊找到了他MBA的同学李明诉说这个苦恼,李明说专业的人做专业的事情才是王道,凉菜的有凉菜厨师、热菜有热菜厨师、主食有主食厨师,如果有新增的菜系菜品,那就再找个专门做这个菜品的厨师就可以了,这样解决了厨师业务能力要求高、饭店扩张慢的问题,孙大熊频频点头,感觉自己花钱买的这个MBA算是瞎了,回去之后进行改进,果然有很大的改善,孙大熊又开始嘚瑟了。

上面的场景其实和高并发网络模型很相似,如果我们在epoll/kqueue的基础上进行业务区分,并且对每一种业务设置相应的处理函数,每次来任务之后对任务进行识别和分发,每种处理函数只处理一种业务,这种模型更加符合OO的设计理念,这也是Reactor反应堆模式的设计思路。


3. Reactor模式

  基于Reactor的组件阵营非常强大:
  • Java NIO

  • Netty

  • libevent/libuv

  • Redis


反应堆模式是一种对象行为的设计模式,主要同于同步IO,异步IO有Proactor模式,这里不详细讲述Proactor模式,二者的主要区别就是Reactor是同步IO,Proactor是异步IO,理论上Proactor效率更高,但是Proactor模式需要操作系统在内核层面对异步IO进行支持,Linux的Boost.asio就是Proactor模式的代表,Windows有IOCP。

网上比较经典的一张Reactor模式的类图:

图中给出了5个部件分别为:
  • handle 可以理解为读写事件 可以注册到Reactor进行监控

  • Sync event demultiplexer 可以理解为epoll/kqueue/select等作为IO事件的采集器

  • Dispatcher 提供注册/删除事件并进行分发,作为事件分发器

  • Event Handler 事件处理器 完成具体事件的回调 供Dispatcher调用

  • Concrete Event Handler 具体请求处理函数


更简洁的流程如下:
以网络场景为例:
循环前先将待监控的事件进行注册,当监控中的Socket读写事件到来时,事件采集器epoll等IO复用工具检测到并且将事件返回给事件分发器Dispatcher,分发器根据读、写、异常等情况进行分发给事件处理器,事件处理器进而根据事件具体类型来调度相应的实现函数来完成任务。


4. Redis的Reactor实现

Redis处理客户端业务(文件事件)的基本流程:


  • Redis的IO复用的选择

#ifdef HAVE_EVPORT#include "ae_evport.c"#else #ifdef HAVE_EPOLL #include "ae_epoll.c" #else #ifdef HAVE_KQUEUE #include "ae_kqueue.c" #else #include "ae_select.c" #endif #endif#endif
Redis中支持多种IO复用,源码中使用相应的宏定义进行选择,编译时就可以获取当前系统支持的最优的IO复用函数来使用,从而实现了Redis的优秀的可移植特性。

  • Redis的任务事件队列

由于Redis的是单线程处理业务的,因此IO复用程序将读写事件同步的逐一放入队列中,如果当前队列已经满了,那么只能出一个入一个,但是由于Redis正常情况下处理得很快,不太会出现队列满迟迟无法放任务的情况,但是当执行某些阻塞操作时将导致长时间的阻塞,无法处理新任务。


  • Redis事件分派器

事件的可读写是从服务器角度看的,分派看到的事件类型包括:
  • AE_READABLE 客户端写数据、关闭连接、新连接到达

  • AE_WRITEABLE 客户端读数据

特别地,当一个套接字连接同时可读可写时,服务器会优先处理读事件再处理写事件,也就是读优先。


  • Redis事件处理器

Redis将文件事件进行归类,编写了多个事件处理器函数,其中包括:
  • 连接应答处理器:实现新连接的建立

  • 命令请求处理器:处理客户端的新命令

  • 命令回复处理器:返回客户端的请求结果

  • 复制处理器:实现主从服务器的数据复制


  • Redis C/S一次完整的交互

Redis服务器的主线程处于循环中,此时一个Client向Redis服务器发起连接请求,假如是6379端口,监听端口在IO复用工具下检测到AE_READABLE事件,并将该事件放入TaskQueue中,等待被处理,事件分派器获取这个读事件,进一步确定是新连接请求,就将该事件交给了连接应答处理器建立连接;
建立连接之后Client继续向服务器发送了一个get命令,仍然被IO复用检测处理放入队列,被事件分派器处理指派给命令请求处理器,调用相应程序进行执行;
服务器将套接字的AE_WRITEABLE事件与命令回复处理器相关联,当客户端尝试读取结果时产生可写事件,此时服务器端触发命令回复响应,并将数据结果写入套接字,完成之后服务端接触该套接字与命令回复处理器之间的关联;

5.参考资料

  • https://www.cnblogs.com/xudong-bupt/p/9463804.html

  • https://www.s0nnet.com/archives/deep-understanding-of-reactor-design-patterns

  • https://cloud.tencent.com/developer/article/1345064

  • https://tech.meituan.com/2016/11/04/nio.html



往期精彩:

浅析Redis 4.0新特性之LazyFree
理解Redis持久化
Linux中的各种锁及其基本原理
浅析CPython的全局解释锁GIL
浅谈Linux下Socket选项设置
深入理解IO复用之epoll


阅读、关注、分享是对作者最大的鼓励!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存