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,主要是因为:
- 足够高效:在 Redis 这种内存操作极快的场景下,I/O 多路复用的性能已经足够,引入 AIO 的复杂度得不偿失。
- 平台兼容性: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 模式):
-
监听与接收(
epoll_wait):- 服务员不主动去问每一桌客人要不要点菜。他只需要坐在前台,看着呼叫铃系统的屏幕。
- 当有新的客人进来时(新连接到来),呼叫铃会“叮”一声,服务员就去接待一下,安排好座位,并把这个客人的需求(Socket)注册到呼叫铃系统中,告诉系统:“如果这桌客人要點菜(连接可读),就通知我”。
- 然后服务员又回去看着屏幕。他不会阻塞在某一桌客人上。
-
事件分发与处理(事件循环):
- 服务员一直盯着呼叫铃的屏幕(这是一个 `while(true)`` 循环,称为事件循环 - Event Loop)。
- 屏幕上突然显示“5号桌要點菜”(某个 Socket 可读了),服务员就过去拿起5号桌的菜单(读取客户端发送的命令请求数据)。
-
命令执行(CPU 计算):
- 服务员拿着菜单跑到后厨(执行命令,如
GET,SET)。因为后厨(Redis 的数据都在内存里)效率极高,瞬间就做好了菜。
- 服务员拿着菜单跑到后厨(执行命令,如
-
返回结果(写事件):
- 菜做好了,服务员并不会立刻端上去,因为客人可能还在吃前菜。他会把5号桌的菜放在出餐口,并再次告诉呼叫铃系统:“等5号桌准备好收菜了(Socket 可写),再通知我”。
- 呼叫系统通知5号桌可写了,服务员就把菜端上去(将响应数据写入 Socket)。
这个过程的核心在于:这个超级服务员(单线程)绝大部分时间都在“等待通知”(epoll_wait),而不是真正在干活。一旦有通知到来,他就去处理,处理速度又极快(内存操作)。因此,他一个人就能高效、有序地服务成百上千桌客人(数万甚至数十万的并发连接)。
技术组件对应关系:
- 超级服务员:Redis 的单线程主事件循环。
- 呼叫铃系统:I/O 多路复用技术(Linux 上是
epoll)。 - 客人点菜/上菜:Socket 上的读/写事件。
- 后厨炒菜:在内存中执行命令(
set,get,hset等)。
epoll 的底层数据结构:红黑树 + 链表
- 红黑树:管理所有注册的 FD
作用:存储用户通过 epoll_ctl 注册的所有 FD,便于快速查找、添加、删除。
特点:
每个节点对应一个 FD,包含其对应的内核 file 结构和事件参数(如可读、可写)。
红黑树是 平衡二叉树,插入、删除、查询的时间复杂度均为 O(logN),比 select/poll 的线性操作(O (N))高效得多。 - 链表(就绪队列) :管理活跃的 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 的处理流程如下:
-
Redis 启动时的初始化
-
创建 epoll 实例:
Redis 在启动时通过 epoll_create 系统调用创建一个 epoll 实例,用于管理所有客户端连接的 I/O 事件。 -
监听套接字绑定:
Redis 绑定监听套接字(如默认端口 6379),并通过 epoll_ctl 将监听套接字注册到 epoll 实例中,监听 读事件(客户端连接请求)。 -
客户端连接阶段-Client A、B、C 发起连接请求
-
事件触发:
当客户端发起连接时,监听套接字会产生 读事件,epoll 检测到该事件并通知 Redis 主进程。 -
处理连接请求:
Redis 主进程通过 accept 系统调用接受连接,创建对应的 客户端套接字描述符(fd),并执行以下操作:
为每个客户端创建 client 结构体,保存连接状态、缓冲区等信息。
通过 epoll_ctl 将客户端套接字注册到 epoll 实例中,监听 读事件(等待客户端发送命令)。 -
客户端发送命令阶段-Client A、B、C 同时发送命令
-
事件触发:
当客户端套接字有数据可读时(命令到达),epoll 检测到对应套接字的 读事件,并将事件放入事件队列。 -
Redis 处理读事件:
Redis 主进程通过 epoll_wait 阻塞等待事件,当事件队列中有事件时(如 3 个读事件),按以下步骤处理:
1. 遍历事件队列:
依次处理每个触发读事件的客户端套接字。
2. 读取命令数据:
通过 read 系统调用从客户端套接字读取命令数据(如 GET key),存入客户端的输入缓冲区。
3. 解析命令:
解析输入缓冲区中的命令,确定操作类型(如 GET、SET、LPUSH)和参数。
4. 执行命令:
根据命令类型操作内部数据结构(如字典、列表),生成响应结果(如 OK、具体数据)。
5. 注册写事件:
将响应结果写入客户端的输出缓冲区,并通过 epoll_ctl 将客户端套接字的 写事件 注册到 epoll 实例中,等待套接字可写时发送响应。 -
向客户端返回响应阶段-准备发送响应结果
-
事件触发:
当客户端套接字可写时(如 TCP 缓冲区有空闲空间),epoll 检测到 写事件,通知 Redis 主进程。 -
Redis 处理写事件:
主进程遍历触发写事件的客户端套接字,执行以下操作:
1. 发送响应数据:
通过 write 系统调用将输出缓冲区中的响应结果(如 +OK\r\n)发送给客户端。
2. 清理缓冲区:
若响应数据全部发送完毕,清除输出缓冲区,并将套接字重新注册为 读事件(等待下一次命令);若未完全发送,保留剩余数据,等待下一次写事件触发时继续发送。
面试常见问题
-
Redis 是单线程的,为什么还能这么快?
- 核心答案:Redis 的快主要得益于以下几点,单线程只是其中之一。
- 纯内存操作:数据存储在内存中,读写速度极快。
- I/O 多路复用:使用
epoll这样的机制高效处理海量连接,避免了多线程的上下文切换和竞争开销。 - 单线程模型:
- 避免了多线程的上下文切换(CPU 切换线程需要保存和加载状态)和锁竞争的开销。
- 保证了每个操作的原子性,不需要考虑并发问题,简化了实现。
- 高效的数据结构:Redis 内置了多种精心设计的数据结构(如跳跃表、压缩列表、哈希表),操作效率极高。
- 核心答案:Redis 的快主要得益于以下几点,单线程只是其中之一。
-
Redis 既然是单线程,如何利用多核 CPU?
- 一个 Redis 实例无法利用多核。但可以通过以下方式:
- 在机器上部署多个 Redis 实例,组成主从集群或分片集群,让不同实例运行在不同 CPU 核心上。
- 使用 Redis 的后台线程:在 Redis 4.0 之后,一些耗时的操作(如
UNLINK删除大键、FLUSHDB ASYNC等)被放到后台线程处理,不阻塞主线程。但网络 I/O 和命令执行仍然在主线程。
- 一个 Redis 实例无法利用多核。但可以通过以下方式:
-
Redis 的 I/O 多路复用底层是怎么实现的?在不同平台一样吗?
- Redis 会自动选择当前系统性能最高的 I/O 多路复用函数作为底层实现。
- 它提供了多种选择,优先级通常是:
evport(Solaris) >epoll(Linux) >kqueue(BSD/MacOS) >select(跨平台,但性能最差)。 epoll相比古老的select和poll的优势在于:- 无需遍历:
select需要遍历所有文件描述符来检查状态,epoll只返回就绪的文件描述符。 - 高效:使用红黑树和双向链表管理描述符,性能不会随着连接数的增加而线性下降。
- 无需遍历:
-
Redis 单线程模型的优缺点是什么?
- 优点:
- 简化实现,无需处理锁和线程同步问题。
- 避免了上下文切换和竞争带来的性能损耗。
- 缺点:
- 无法充分发挥多核 CPU 的性能,但可以通过集群化解。
- 如果某个命令执行过慢(如
keys *),会阻塞整个服务器,所有其他请求都必须等待。
- 优点:
-
什么是 Reactor 模式?
- Reactor 模式是一种处理并发 I/O 的事件处理模式。
- 其核心思想是:“不要用电话来找我们,我们会打电话(回调)给你”。
- 它有一个主循环(Reactor),通过 I/O 多路复用接口监听所有事件。当某个事件(如可读)就绪时,Reactor 就分发给相应的处理器(Handler)来处理。

浙公网安备 33010602011771号