redis IO多路复用

Redis 的 I/O 模型是什么?

Redis 的 I/O 模型是基于 I/O 多路复用Reactor 模式,它属于 同步非阻塞 I/O(NIO)不是真正的 AIO(异步 I/O)

  • NIO (Non-blocking I/O): 这是 Redis 使用的模型。应用程序通过系统调用(如 select, poll, epoll)不断轮询内核,询问哪些 Socket 准备好了(可读/可写),然后应用程序自己再去读取数据到缓冲区并进行处理。数据的“移动”(从内核缓冲区到用户缓冲区)仍然是同步的、需要进程自己发起的。
  • AIO (Asynchronous I/O): 应用程序发起一个 I/O 操作后,立即返回,可以去做别的事。内核会自己完成所有工作(包括将数据从网络读取到内核缓冲区,再拷贝到用户指定的缓冲区),完成后通过信号或回调函数通知应用程序。整个过程应用程序都不需要参与数据拷贝的等待。

Redis 没有使用 AIO,主要是因为:

  1. 足够高效:在 Redis 这种内存操作极快的场景下,I/O 多路复用的性能已经足够,引入 AIO 的复杂度得不偿失。
  2. 平台兼容性:Linux 上的 AIO 实现(libaio)对网络 Socket 的支持并不完善,主要针对文件 I/O。

I/O 多路复用机制

Redis 是通过I/O多路复用机制来管理大量客户端连接。这使得redis可以实现通过单线程来处理多个客户端连接的请求,避免了为每个客户端创建独立的线程,从而减少了上下文切换的开销,提高了系统的并发性和性能。
理解:redis是通过一个线程来处理多个客户端的请求(通过 I/O 多路复用实现),意在减少CPU上下文的切换,提升系统的并发性和性能。

文件描述符

文件描述符: 是操作系统为每个打开的文件、套接字、管道等 I/O 资源分配的一个整数标识符。它用于唯一标识进程与某个 I/O 资源之间的连接。
文件描述符是操作系统内核管理 I/O 资源的方式,应用程序通过文件描述符与这些资源进行交互。

简单理解下:
一个进程可以访问多个文件(或其他网络终端或通道连接等)资源,这些访问都需要进程与文件建立连接后才能进行数据传输。操作系统为了能清晰区分这些连接,不导致数据传输错乱,会给每一个的这种连接(进程和文件通信)都添加一个唯一整数标识,就是文件描述符。


I/O 多路复用与 Reactor 模式

可以把 Redis 的工作模式想象成一个非常高效的餐厅服务员。

1. 传统阻塞 I/O 模型(对比)

就像一家餐厅一个服务员服务一桌客人。服务员必须站在客人旁边,等客人点完菜(数据准备好),才能去服务下一桌。中间等待的时间完全被浪费了。餐厅能接待的客人数量(并发连接数)直接取决于服务员(线程)的数量。

2. Redis 的 I/O 多路复用模型(高效的服务员)

这家餐厅只有一个超级服务员(单线程),但他有一个神器:呼叫铃系统(I/O 多路复用器,如 epoll

他的工作流程是这样的(这就是 Reactor 模式):

  1. 监听与接收(epoll_wait

    • 服务员不主动去问每一桌客人要不要点菜。他只需要坐在前台,看着呼叫铃系统的屏幕。
    • 当有新的客人进来时(新连接到来),呼叫铃会“叮”一声,服务员就去接待一下,安排好座位,并把这个客人的需求(Socket)注册到呼叫铃系统中,告诉系统:“如果这桌客人要點菜(连接可读),就通知我”。
    • 然后服务员又回去看着屏幕。他不会阻塞在某一桌客人上。
  2. 事件分发与处理(事件循环)

    • 服务员一直盯着呼叫铃的屏幕(这是一个 `while(true)`` 循环,称为事件循环 - Event Loop)。
    • 屏幕上突然显示“5号桌要點菜”(某个 Socket 可读了),服务员就过去拿起5号桌的菜单(读取客户端发送的命令请求数据)。
  3. 命令执行(CPU 计算)

    • 服务员拿着菜单跑到后厨(执行命令,如 GET, SET)。因为后厨(Redis 的数据都在内存里)效率极高,瞬间就做好了菜。
  4. 返回结果(写事件)

    • 菜做好了,服务员并不会立刻端上去,因为客人可能还在吃前菜。他会把5号桌的菜放在出餐口,并再次告诉呼叫铃系统:“等5号桌准备好收菜了(Socket 可写),再通知我”。
    • 呼叫系统通知5号桌可写了,服务员就把菜端上去(将响应数据写入 Socket)。

这个过程的核心在于:这个超级服务员(单线程)绝大部分时间都在“等待通知”(epoll_wait),而不是真正在干活。一旦有通知到来,他就去处理,处理速度又极快(内存操作)。因此,他一个人就能高效、有序地服务成百上千桌客人(数万甚至数十万的并发连接)。

技术组件对应关系:

  • 超级服务员:Redis 的单线程主事件循环。
  • 呼叫铃系统:I/O 多路复用技术(Linux 上是 epoll)。
  • 客人点菜/上菜:Socket 上的读/写事件。
  • 后厨炒菜:在内存中执行命令(set, get, hset 等)。

epoll 的底层数据结构:红黑树 + 链表

  1. 红黑树:管理所有注册的 FD
    作用:存储用户通过 epoll_ctl 注册的所有 FD,便于快速查找、添加、删除。
    特点:
    每个节点对应一个 FD,包含其对应的内核 file 结构和事件参数(如可读、可写)。
    红黑树是 平衡二叉树,插入、删除、查询的时间复杂度均为 O(logN),比 select/poll 的线性操作(O (N))高效得多。
  2. 链表(就绪队列) :管理活跃的 FD
    作用:当内核检测到 FD 有事件发生(如客户端发送数据),将该 FD 加入就绪队列,避免遍历整个红黑树。
    特点:
    仅包含「有事件发生的 FD」,epoll_wait 调用时直接从该队列获取数据,时间复杂度为 O(M)(M 为活跃连接数,通常远小于 N)。
    内核 通过 回调机制 维护该队列:当 FD 所属的套接字缓冲区状态变化时(如可读),内核自动将其加入就绪队列。

举例说明

假设当前有 3 个客户端(Client A、B、C)同时向 Redis 服务器发起命令请求(如 GET key、SET key value、LPUSH list item),Redis 的处理流程如下:

  1. Redis 启动时的初始化

  2. 创建 epoll 实例:
    Redis 在启动时通过 epoll_create 系统调用创建一个 epoll 实例,用于管理所有客户端连接的 I/O 事件。

  3. 监听套接字绑定:
    Redis 绑定监听套接字(如默认端口 6379),并通过 epoll_ctl 将监听套接字注册到 epoll 实例中,监听 读事件(客户端连接请求)。

  4. 客户端连接阶段-Client A、B、C 发起连接请求

  5. 事件触发
    当客户端发起连接时,监听套接字会产生 读事件,epoll 检测到该事件并通知 Redis 主进程。

  6. 处理连接请求:
    Redis 主进程通过 accept 系统调用接受连接,创建对应的 客户端套接字描述符(fd),并执行以下操作:
    为每个客户端创建 client 结构体,保存连接状态、缓冲区等信息。
    通过 epoll_ctl 将客户端套接字注册到 epoll 实例中,监听 读事件(等待客户端发送命令)。

  7. 客户端发送命令阶段-Client A、B、C 同时发送命令

  8. 事件触发:
    当客户端套接字有数据可读时(命令到达),epoll 检测到对应套接字的 读事件,并将事件放入事件队列。

  9. Redis 处理读事件:
    Redis 主进程通过 epoll_wait 阻塞等待事件,当事件队列中有事件时(如 3 个读事件),按以下步骤处理:
    1. 遍历事件队列:
    依次处理每个触发读事件的客户端套接字。
    2. 读取命令数据:
    通过 read 系统调用从客户端套接字读取命令数据(如 GET key),存入客户端的输入缓冲区。
    3. 解析命令:
    解析输入缓冲区中的命令,确定操作类型(如 GET、SET、LPUSH)和参数。
    4. 执行命令:
    根据命令类型操作内部数据结构(如字典、列表),生成响应结果(如 OK、具体数据)。
    5. 注册写事件:
    将响应结果写入客户端的输出缓冲区,并通过 epoll_ctl 将客户端套接字的 写事件 注册到 epoll 实例中,等待套接字可写时发送响应。

  10. 向客户端返回响应阶段-准备发送响应结果

  11. 事件触发:
    当客户端套接字可写时(如 TCP 缓冲区有空闲空间),epoll 检测到 写事件,通知 Redis 主进程。

  12. Redis 处理写事件:
    主进程遍历触发写事件的客户端套接字,执行以下操作:
    1. 发送响应数据
    通过 write 系统调用将输出缓冲区中的响应结果(如 +OK\r\n)发送给客户端。
    2. 清理缓冲区:
    若响应数据全部发送完毕,清除输出缓冲区,并将套接字重新注册为 读事件(等待下一次命令);若未完全发送,保留剩余数据,等待下一次写事件触发时继续发送。

面试常见问题

  1. Redis 是单线程的,为什么还能这么快?

    • 核心答案:Redis 的快主要得益于以下几点,单线程只是其中之一。
      • 纯内存操作:数据存储在内存中,读写速度极快。
      • I/O 多路复用:使用 epoll 这样的机制高效处理海量连接,避免了多线程的上下文切换和竞争开销。
      • 单线程模型
        • 避免了多线程的上下文切换(CPU 切换线程需要保存和加载状态)和锁竞争的开销。
        • 保证了每个操作的原子性,不需要考虑并发问题,简化了实现。
      • 高效的数据结构:Redis 内置了多种精心设计的数据结构(如跳跃表、压缩列表、哈希表),操作效率极高。
  2. Redis 既然是单线程,如何利用多核 CPU?

    • 一个 Redis 实例无法利用多核。但可以通过以下方式:
      • 在机器上部署多个 Redis 实例,组成主从集群或分片集群,让不同实例运行在不同 CPU 核心上。
      • 使用 Redis 的后台线程:在 Redis 4.0 之后,一些耗时的操作(如 UNLINK 删除大键、FLUSHDB ASYNC 等)被放到后台线程处理,不阻塞主线程。但网络 I/O 和命令执行仍然在主线程。
  3. Redis 的 I/O 多路复用底层是怎么实现的?在不同平台一样吗?

    • Redis 会自动选择当前系统性能最高的 I/O 多路复用函数作为底层实现。
    • 它提供了多种选择,优先级通常是:evport (Solaris) > epoll (Linux) > kqueue (BSD/MacOS) > select (跨平台,但性能最差)。
    • epoll 相比古老的 selectpoll 的优势在于:
      • 无需遍历select 需要遍历所有文件描述符来检查状态,epoll 只返回就绪的文件描述符。
      • 高效:使用红黑树和双向链表管理描述符,性能不会随着连接数的增加而线性下降。
  4. Redis 单线程模型的优缺点是什么?

    • 优点
      • 简化实现,无需处理锁和线程同步问题。
      • 避免了上下文切换和竞争带来的性能损耗。
    • 缺点
      • 无法充分发挥多核 CPU 的性能,但可以通过集群化解。
      • 如果某个命令执行过慢(如 keys *),会阻塞整个服务器,所有其他请求都必须等待。
  5. 什么是 Reactor 模式?

    • Reactor 模式是一种处理并发 I/O 的事件处理模式。
    • 其核心思想是:“不要用电话来找我们,我们会打电话(回调)给你”
    • 它有一个主循环(Reactor),通过 I/O 多路复用接口监听所有事件。当某个事件(如可读)就绪时,Reactor 就分发给相应的处理器(Handler)来处理。
posted @ 2025-09-19 10:02  adragon  阅读(57)  评论(0)    收藏  举报