为什么 Redis 单线程能支撑高并发?

最近在看 UNIX 网络编程并研究了一下 Redis 的实现 , 感觉 Redis 的源代码十分适合阅读和分析 , 其中 I/O 多路复用(mutiplexing)部分的实现非常干净和优雅 , 在这里想对这部分的内容进行简单的整理 。
几种 I/O 模型为什么 Redis 中要使用 I/O 多路复用这种技术呢?
首先 , Redis 是跑在单线程中的 , 所有的操作都是按照顺序线性执行的 , 但是由于读写操作等待用户输入或输出都是阻塞的 , 所以 I/O 操作在一般情况下往往不能直接返回 , 这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务 , 而 I/O 多路复用就是为了解决这个问题而出现的 。
Blocking I/O先来看一下传统的阻塞 I/O 模型到底是如何工作的:当使用 read 或者 write 对某一个**文件描述符(File Descriptor 以下简称 FD)**进行读写时 , 如果当前 FD 不可读或不可写 , 整个 Redis 服务就不会对其它的操作作出响应 , 导致整个服务不可用 。
这也就是传统意义上的 , 也就是我们在编程中使用最多的阻塞模型:
blocking-io
阻塞模型虽然开发中非常常见也非常易于理解 , 但是由于它会影响其他 FD 对应的服务 , 所以在需要处理多个客户端任务的时候 , 往往都不会使用阻塞模型 。
I/O 多路复用虽然还有很多其它的 I/O 模型 , 但是在这里都不会具体介绍 。
阻塞式的 I/O 模型并不能满足这里的需求 , 我们需要一种效率更高的 I/O 模型来支撑 Redis 的多个客户(redis-cli) , 这里涉及的就是 I/O 多路复用模型了:
I:O-Multiplexing-Mode
在 I/O 多路复用模型中 , 最重要的函数调用就是 select , 该方法的能够同时监控多个文件描述符的可读可写情况 , 当其中的某些文件描述符可读或者可写时 , select 方法就会返回可读以及可写的文件描述符个数 。
关于 select 的具体使用方法 , 在网络上资料很多 , 这里就不过多展开介绍了;
与此同时也有其它的 I/O 多路复用函数 epoll/kqueue/evport , 它们相比 select 性能更优秀 , 同时也能支撑更多的服务 。
Reactor 设计模式Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)
【为什么 Redis 单线程能支撑高并发?】redis-reactor-pattern
文件事件处理器使用 I/O 多路复用模块同时监听多个 FD , 当 accept、read、write 和 close 文件事件产生时 , 文件事件处理器就会回调 FD 绑定的事件处理器 。
虽然整个文件事件处理器是在单线程上运行的 , 但是通过 I/O 多路复用模块的引入 , 实现了同时对多个 FD 读写的监控 , 提高了网络通信模型的性能 , 同时也可以保证整个 Redis 服务实现的简单 。
I/O 多路复用模块I/O 多路复用模块封装了底层的 select、epoll、avport 以及 kqueue 这些 I/O 多路复用函数 , 为上层提供了相同的接口 。
ae-module
在这里我们简单介绍 Redis 是如何包装 select 和 epoll 的 , 简要了解该模块的功能 , 整个 I/O 多路复用模块抹平了不同平台上 I/O 多路复用函数的差异性 , 提供了相同的接口:

  • static int aeApiCreate(aeEventLoop *eventLoop)
  • static int aeApiResize(aeEventLoop *eventLoop, int setsize)
  • static void aeApiFree(aeEventLoop *eventLoop)
  • static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)
  • static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask)
  • static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
同时 , 因为各个函数所需要的参数不同 , 我们在每一个子模块内部通过一个 aeApiState 来存储需要的上下文信息:
// selecttypedef struct aeApiState {fd_set rfds, wfds;fd_set _rfds, _wfds;} aeApiState;// epolltypedef struct aeApiState {int epfd;struct epoll_event *events;} aeApiState;这些上下文信息会存储在 eventLoop 的 void *state 中 , 不会暴露到上层 , 只在当前子模块中使用 。
封装 select 函数select 可以监控 FD 的可读、可写以及出现错误的情况 。
在介绍 I/O 多路复用模块如何对 select 函数封装之前 , 先来看一下 select 函数使用的大致流程:
int fd = /* file descriptor */fd_set rfds;FD_ZERO(FD_SET(fd,; ) {select(fd+1,if (FD_ISSET(fd,--tt-darkmode-color: #A3A3A3;">初始化一个可读的 fd_set 集合 , 保存需要监控可读性的 FD;
  • 使用 FD_SET 将 fd 加入 rfds;
  • 调用 select 方法监控 rfds 中的 FD 是否可读;
  • 当 select 返回时 , 检查 FD 的状态并完成对应的操作 。
  • 而在 Redis 的 ae_select 文件中代码的组织顺序也是差不多的 , 首先在 aeApiCreate 函数中初始化 rfds 和 wfds: