Redis 经常被描述为一个高性能的内存数据库。一个常见的问题是:Redis 使用单线程模型,却能够同时处理大量客户端连接,并保持很高的吞吐能力,其原因是什么?

答案并不是简单的“Redis 很快”。Redis 的高性能来自多个设计的共同作用:

  • 内存数据结构;
  • 高效的数据协议 RESP;
  • 单线程命令执行模型;
  • 基于事件驱动的 Event Loop;
  • 操作系统提供的 I/O 多路复用机制。

其中最核心的设计思想是:

Redis 并不是让大量线程同时执行命令,而是让一个线程高效管理大量网络连接,并按照事件顺序执行命令。

本文将从一个 Redis 请求出发,分析客户端发送请求之后,Redis 如何通过 TCP Socket 接收数据、借助 epoll 发现事件、进入 Event Loop,最终执行命令并返回结果。理解这条完整链路,也就理解了 Redis 单线程模型能够支撑高并发的根本原因。

1. 单线程与事件驱动的由来

1.1 为什么需要事件驱动模型

Redis 的主要工作模式是:

客户端 → 发送命令 → Redis 接收请求 → 执行命令 → 返回结果

例如执行:

GET user:1001

其过程大致为:解析命令 → 查找 Key → 访问内存数据结构 → 生成响应。这些操作本身非常快。Redis 真正需要解决的问题是:如何高效管理大量客户端连接。

假设有 10000 个客户端同时连接 Redis。若采用传统方式“一个连接对应一个线程”,则需要 10000 个线程,随之产生:

  • 线程创建成本;
  • 内存占用;
  • 上下文切换;
  • 多线程同步复杂度。

因此 Redis 选择了另一种设计:

大量客户端连接
        ↓
   I/O 多路复用
        ↓
  单线程 Event Loop
        ↓
  顺序执行 Redis 命令

1.2 “单线程”到底指什么

许多文章会简单描述“Redis 是单线程数据库”。这一说法并不准确。更精确的表述是:Redis 的核心命令执行模型是单线程的

所有常规 Redis 命令——GETSETINCRLPUSHZADD 等——默认都由主线程串行执行。例如:

Client A: SET user:name Tom
Client B: GET user:name

Redis:执行 SET → 执行 GET

不会出现多个线程同时修改内部数据结构的情况。这一设计带来的直接好处是:

  • 不需要为大多数数据结构引入大量锁;
  • 避免线程竞争;
  • 保证命令执行顺序;
  • 简化数据结构设计。

需要强调的是,Redis 并非所有工作都在单线程中完成。网络 I/O、后台持久化任务、大对象的异步删除等,在不同版本中已经引入多线程或多进程优化。尤其是 Redis 6:

网络 I/O 可以使用多线程,但命令执行仍然保持单线程。

理解“单线程”的边界,是后续讨论事件循环与 I/O 多路复用的前提。

2. 整体架构与模型对比

2.1 单线程事件循环的整体架构

Redis 的核心运行时可以拆分为三个部分:I/O 多路复用器、Event Loop 以及 Command Execution。

Redis 单线程事件循环架构

I/O 多路复用器负责发现哪些客户端连接已经准备好处理。例如:

Client A 有数据
Client B 没有数据
Client C 有数据

epoll 会告知 Redis:Client A 与 Client C 已经就绪。

Event Loop负责调度已经发生的事件。它不会主动扫描全部连接,而是:

等待事件 → 获取就绪连接 → 处理请求

Command Execution负责执行 Redis 命令。例如 GET user:1001,最终会访问内存中的数据结构并生成响应。

三者的协作关系可以概括为:

客户端连接 → I/O 多路复用 → Event Loop → 执行命令 → 返回响应

2.2 从传统线程模型到事件驱动模型

传统线程模型有其优点,但也存在明显限制,包括上下文切换开销以及资源竞争时的锁成本。Redis 与 Node.js 等系统采用事件循环模型,以较低的资源开销获得更高的并发效率。

传统线程模型 vs Redis 事件循环模型

传统服务器常见模型是“一个连接对应一个线程”:

Client A → Thread A
Client B → Thread B
Client C → Thread C

连接数量增加时:连接数量 ↑ → 线程数量 ↑ → 上下文切换增加。

Redis 使用的事件驱动模型则是:

Client A
Client B
Client C
    ↓
I/O 多路复用
    ↓
Event Loop
    ↓
执行命令

核心变化在于:从一个线程服务一个连接,变为一个线程管理多个连接

需要特别指出:I/O 多路复用本身并不是多线程。它解决的问题是——一个线程如何高效地等待多个网络连接上的数据到达。

3. I/O 多路复用与 epoll 工作机制

一个 Redis 主线程如何同时管理成千上万个客户端连接?答案是 I/O 多路复用(I/O Multiplexing)。它是 Redis 高并发网络模型的核心。

3.1 阻塞 I/O:一个线程等待一个连接

最传统的网络模型是阻塞 I/O。其工作流程为:

线程 → 等待客户端数据 → 读取数据 → 处理请求 → 返回响应

若 Client A 对应 Thread A,且客户端迟迟不发送数据,线程只能阻塞等待。因此:

10000 个客户端 → 10000 个线程

会造成大量线程资源消耗、CPU 在线程切换中的浪费,以及内存占用的增加。

3.2 非阻塞 I/O:主动轮询

为避免线程一直等待,可以采用非阻塞 I/O,由一个线程不断检查:

while (true) {
    check socket1;
    check socket2;
    check socket3;
}

线程不再阻塞,但产生了新的问题。若有 10000 个 socket,线程需要不断遍历 socket1 到 socket10000。即使绝大多数连接没有数据,仍需反复检查,造成大量无效的 CPU 消耗。

3.3 I/O 多路复用:由操作系统通知事件

I/O 多路复用改变了思路。Redis 不再主动询问“socket1 有数据吗?socket2 有数据吗?”,而是告知操作系统:请监听这些 socket,有事件发生时再通知我。

工作流程为:

Redis 线程 → I/O 多路复用器 → 多个 Socket

操作系统负责监听连接状态、检测数据到达,并返回已经准备好的连接。Redis 只处理真正发生事件的连接。

3.4 select、poll 与 epoll

Linux 提供多种 I/O 多路复用机制,包括 select、poll 和 epoll。

select 的基本思路是:应用程序提交全部 socket → 内核检查 → 返回结果。每次调用都需要传递所有 socket,内核需遍历整个列表,时间复杂度为 O(n)。连接数增多后效率明显下降。

poll 改进了 select 的数据结构限制,但核心问题未变:每次调用仍需遍历所有连接,复杂度仍为 O(n)。

epoll 是 Linux 下 Redis 通常使用的机制。其核心思想是:注册一次事件,之后只返回真正发生变化的连接

3.5 epoll 的工作流程

I/O 多路复用使一个线程能够同时监听多个网络连接,并只处理有事件的连接,它是事件循环机制的先决条件之一。

I/O 多路复用与 epoll 工作方式

epoll 可以理解为:

Redis → epoll 实例 → Linux Kernel → 多个 Socket

epoll_create:创建监听实例

Redis 首先创建 epoll 实例:

epoll_create()

可将其理解为创建一个事件管理器。

epoll_ctl:注册监听事件

客户端连接建立后:

Client → Socket → epoll_ctl()

Redis 告知 Linux 监听该 socket。例如 socket1、socket2、……、socket10000 都会被注册到 epoll。

epoll_wait:等待事件发生

主线程进入:

epoll_wait()

此时 Redis 无需主动检查各个 socket,而是进入等待状态,由内核负责监听。

事件发生后返回就绪连接

例如客户端 B 发送 GET user:1001,内核发现 socket B 可读,于是:

epoll_wait() → 返回 socket B

Redis 只处理 socket B,而无需扫描全部连接。

3.6 epoll 高效的原因

epoll 高效的关键,不在于“快一点”,而在于它改变了事件通知模型。

传统模型是应用程序主动询问所有连接,找到有数据的连接——类似反复询问“你有没有消息?”。

epoll 则是应用程序等待通知,只处理发生事件的连接——类似“有消息时再告诉我”。

可以将 epoll 简化为两个集合:

epoll instance
+----------------------+
|  Interest List       |
|  socket1             |
|  socket2             |
|  socket3             |
+----------------------+

事件发生后
+----------------------+
|  Ready List          |
|  socket2             |
|  socket5             |
+----------------------+

Interest List 表示 Redis 希望监听的 socket 集合,Ready List 表示当前已经准备好处理的 socket 集合。因此 epoll_wait() 返回的是 Ready List,而不是全部连接列表。这正是 epoll 在高连接数场景下仍然高效的原因。

4. 一个 Redis 请求的完整生命周期

理解 I/O 多路复用之后,可以把一次完整请求的路径串联起来。

Redis 请求生命周期流程图

假设客户端执行:

GET user:1001

整体过程为:

Client → TCP Socket → Linux Kernel → epoll
→ Redis Event Loop → RESP 解析 → Command 执行
→ 生成响应 → 返回客户端

客户端发送请求

客户端通过 TCP 发送 GET user:1001,数据进入:

客户端 → TCP 连接 → Redis Socket

此时主线程并不知道哪个连接有数据,它正阻塞在事件等待上。

Linux 内核接收网络数据

网络数据首先进入内核的 TCP 接收缓冲区。Redis 用户空间线程不会直接操作网卡。数据路径为:

网卡 → Linux Kernel → Socket Buffer → Redis

epoll 发现事件

主线程执行 epoll_wait(),内核返回 socket readable,表示该连接已有数据可读。

Event Loop 调度请求

Event Loop 收到事件后:

socket ready → read() → 获取客户端数据

随后进入命令处理流程。

RESP 协议解析

Redis 使用 RESP(Redis Serialization Protocol)与客户端通信。客户端发送的 GET user:1001 被解析为:

Command: GET
Key:     user:1001

并转换为内部命令结构。

执行 Redis 命令

进入命令执行阶段:

Command Table → Command Handler → Redis Data Structure

以 GET 为例:查找哈希表 → 获取 Redis 对象 → 生成结果。由于命令在主线程中串行执行,无需额外加锁即可保证数据结构的一致性。

返回响应

执行完成后:

Redis → TCP Socket → Client

客户端收到响应,一次请求的生命周期结束。

读缓冲区与写缓冲区的存在,使网络读写与命令执行在时间上可以进一步解耦。即使某个客户端网络暂时拥塞,也不一定立刻阻塞整个 Event Loop;当然,输出缓冲区过大时仍需有相应的保护机制。

5. Event Loop 的内部实现

Redis 的核心循环可以抽象为:

while (server.running) {
    events = aeProcessEvents();
    processCommand();
    serverCron();
}

其中 aeProcessEvents() 负责等待网络事件、获取文件事件并调度处理,是事件驱动模型的核心。

从使用者视角看,请求路径是:

客户端 → TCP Socket → epoll → Event Loop → 执行命令 → 返回响应

进入内部实现,事件循环由两类事件构成:

  • 文件事件(File Event)
  • 时间事件(Time Event)

可抽象为:

Redis Event Loop
        ↓
   +----+----+
   ↓         ↓
File Events  Time Events
   ↓         ↓
网络请求处理  定时任务处理

文件事件

Redis 中的大部分网络请求属于文件事件。这里的“文件”并非普通磁盘文件。在 Unix/Linux 中:

Socket 也被抽象为文件描述符(File Descriptor)。

因此网络请求的处理路径可以描述为:

客户端连接 → Socket → File Descriptor → Redis Event Loop

Redis 通过事件机制监听:新连接建立、客户端可读事件、客户端可写事件。

与 epoll 的关系

Redis 使用一层事件抽象屏蔽不同操作系统的差异。Linux 使用 epoll,macOS 使用 kqueue,Windows 使用 IOCP。上层逻辑并不直接调用 epoll,调用关系为:

Redis Event API → 操作系统事件机制 → epoll / kqueue

从而保证跨平台运行。

源码中的处理流程

事件循环相关代码主要位于 src/ae.c,核心函数是 aeProcessEvents(),其职责为:

  1. 等待事件;
  2. 获取就绪事件;
  3. 调用对应的事件处理函数。

简化流程为:

aeProcessEvents()
        ↓
   aeApiPoll()
        ↓
  epoll_wait()
        ↓
  返回就绪事件
        ↓
  执行事件回调

aeApiPoll() 是不同操作系统事件模型的封装,在 Linux 下最终调用 epoll_wait()

6. 单线程为何能保持高性能,又有哪些限制

6.1 高性能的原因

Redis 的高性能并非源于“单线程比多线程更快”。在 CPU 密集型任务中,多线程通常更有优势。Redis 选择单线程,是因为它所面对的问题具有以下特征:

大量网络连接 + 大量短小命令 + 内存数据操作

在这种场景下,降低并发复杂度往往比追求 CPU 并行更重要

避免锁竞争

若使用多个线程同时执行命令,例如:

Thread A: SET user:name Tom
Thread B: GET user:name

多个线程并发访问 Hash、List、Sorted Set 等结构时,就需要引入 mutex、rwlock 以及相应的数据同步,复杂度显著上升。单线程模型下命令天然串行:

Command A → Command B → Command C

减少上下文切换

线程切换需要保存与恢复 CPU 寄存器、栈信息与调度状态。大量线程频繁切换会消耗可观的 CPU 时间。单线程模型消除了这类开销。

将网络等待交给操作系统

Redis 不会轮询“是否有客户端请求”,而是调用 epoll_wait() 等待事件。网络就绪检测由内核完成,主线程只在有事件时被唤醒,把 CPU 时间集中用于命令执行。

内存数据结构的高效

数据主要驻留在内存中。一次普通的 GET user:1001 通常不涉及磁盘访问、跨服务网络调用或复杂计算,主要路径是:

Key 哈希计算 → 内存查找 → 返回对象

因此单线程已足以支撑很高的请求量。

6.2 单线程模型的限制

单线程在带来简单性与效率的同时,也存在明显限制:

一个耗时操作会影响所有客户端。

慢命令阻塞事件循环

例如 KEYS * 需要遍历全部键空间。执行期间主线程无法处理其他请求。生产环境应优先使用 SCAN 进行渐进式遍历。

大 Key 操作

若一个 List 包含百万级元素,执行 DEL huge:list 可能耗时较长,期间 Event Loop 会被阻塞。Redis 提供了 UNLINK,将实际释放工作放到后台执行,以减轻对主线程的影响。

Lua 脚本阻塞

Lua 脚本在主线程中原子执行。脚本若包含大量循环或复杂计算,同样会阻塞其他客户端。编写脚本时应保持逻辑简短,避免长时间运行。

此外,Redis 提供了 SLOWLOG、延迟监控等工具,用于发现和定位阻塞来源。理解这些限制,才能在使用中避开单线程模型的尖锐边界。

7. Redis 6 的多线程 I/O 演进

Redis 6 引入了多线程 I/O。需要明确的是:

Redis 6 并没有把命令执行改为多线程。

命令执行仍然由主线程串行完成,发生变化的是网络数据的读写。

Redis 6 多线程 I/O 架构

引入多线程 I/O 的原因

早期 Redis 的瓶颈更多在 CPU。随着硬件能力提升,命令执行本身越来越快,网络读写逐渐成为新的瓶颈:

CPU 能力提升 → 命令执行越来越快 → 网络读写成为瓶颈

因此 Redis 6 的优化重点放在网络读取与网络写回两端。

工作流程

新的大致模型为:

Client Connections
        ↓
I/O Threads(read / write)
        ↓
Main Thread(命令执行)
        ↓
内存数据结构

可以理解为:I/O 线程负责搬运数据,主线程负责思考与执行。

为何不将命令执行也多线程化

Redis 的重要价值之一是简单的数据模型。若命令执行多线程化,需要解决数据竞争、锁粒度、执行顺序以及事务(MULTI/EXEC)语义等一系列问题。当前设计在性能与复杂度之间取得了务实的平衡,因此仍然保留单线程命令执行。

8. 与 Node.js 的相似设计及总结

Redis 的事件循环与 Node.js 存在相似的设计思想。

Node.js:

JavaScript Thread → Event Loop → libuv → epoll

Redis:

Redis Main Thread → Event Loop → aeEventLoop → epoll

两者的共同之处在于:使用单线程处理业务逻辑,通过操作系统的 I/O 多路复用管理大量连接。


Redis 的高性能架构可以概括为:

                Redis
        +----------------+
        |                |
     Memory         Event Driven
        |                |
  数据结构优化        I/O 多路复用
        |                |
     内存存储         Event Loop
                         |
                    epoll / kqueue
                         |
                  单线程命令执行

Redis 并不是因为“使用了单线程”才快。其真正的设计思想是:

将网络连接管理交给操作系统,将事件调度交给 Event Loop,将命令执行保持为简单的单线程模型。

一次请求的完整路径是:

Client → TCP Socket → Linux Kernel → epoll → Event Loop
→ RESP Parser → Command Execution → Redis Data Structure
→ Response → Client

理解这条链路,就理解了 Redis 为何能够以相对简单的单线程模型支撑高并发场景。