Redis 线程模型
一、核心结论:单线程命令处理 + 多线程辅助
Redis 的线程模型常被概括为“单线程”,但这其实是一个逐渐过时的说法。准确描述应该是:
- Redis 6.0 之前:完全单线程
- Redis 6.0 之后:
- 网络 I/O 读写:多线程
- 命令执行:仍然单线程
所以 Redis 始终不存在命令执行层面的并发竞争,也就不需要锁,这是它快的关键。
二、为什么 Redis 选择单线程执行命令?
2.1 历史原因与设计哲学
- CPU 不是瓶颈:Redis 的性能瓶颈通常是内存和网络带宽,而非 CPU 核心数。
- 避免锁竞争:多线程需要处理数据竞争,引入锁会降低性能、增加复杂度和调试难度。
- 简单可靠:单线程模型下,命令天然串行,无需考虑事务隔离、死锁等问题,开发维护成本低。
2.2 单线程为什么能那么快?
- 内存操作:所有数据在内存中,读写极快(纳秒级)。
- 高效数据结构:专门优化的哈希表、跳表、压缩列表等,时间复杂度 O(1) 或 O(logN)。
- 非阻塞 I/O 多路复用:单线程通过 epoll/kqueue 同时监听多个连接,处理海量并发。
- 避免上下文切换:没有线程切换开销和 CPU 缓存失效。
实测单节点 Redis 可达 10 万+ QPS(纯内存操作),足以应对绝大多数场景。
三、Redis 单线程模型核心
Redis 将客户端的连接、读写请求抽象为文件事件,利用 I/O 多路复用 程序监听多个套接字,并将事件派发给对应的事件处理器。这个模型称为 Reactor 模式。
3.1 四个核心组件
┌─────────────────────────────────────────────────┐
│ Redis 主线程 │
│ ┌──────────┐ ┌───────────────┐ │
│ │ 套接字 │───▶│ I/O多路复用 │ │
│ │ (Socket) │ │ (epoll/select)│ │
│ └──────────┘ └───────┬───────┘ │
│ │ 事件通知 │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 文件事件分派器 │ │
│ └─────────┬───────────┘ │
│ │ │
│ ┌───────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│ │连接应答 │ │命令请求 │ │命令回复 ││
│ │处理器 │ │处理器 │ │处理器 ││
│ └──────────┘ └──────────┘ └──────────┘│
└─────────────────────────────────────────────────┘
3.1.1 套接字(Socket)
每个客户端连接对应一个套接字,Redis 会监听其上的可读、可写事件。
3.1.2 I/O 多路复用程序
封装了底层的 select、epoll、evport、kqueue 等系统调用。它负责:
- 监听多个套接字
- 当套接字就绪(可读/可写),通知文件事件分派器
Redis 会根据系统自动选择最高效的多路复用函数(Linux 下为 epoll)。
3.1.3 文件事件分派器
接收多路复用程序传来的事件,根据事件类型(读/写)和套接字,调用对应的事件处理器。
3.1.4 事件处理器
- 连接应答处理器:处理客户端连接请求(accept)。
- 命令请求处理器:读取客户端发送的命令。
- 命令回复处理器:将命令执行结果写回客户端。
3.2 文件事件类型
文件事件有两种类型:读事件(AE_READABLE)、写事件(AE_WRITABLE)。
- AE_READABLE:主要指的是客户端发送到redis服务器的所有请求命令(包含get、set等等命令)时产生的事件。
- AE_WRITABLE:主要指的是redis执行完客户端发送的请求命令后,返回执行结果到客户端是产生的事件。
四、事件处理流程
在 Redis 6.0 之前,网络 I/O 读写和命令执行都在同一个主线程中完成。流程如下:
客户端请求 → 主线程通过 epoll 感知可读 → 读取数据(单线程)→ 解析命令 → 执行命令 → 写回响应(单线程)
这种模型在处理大量并发连接时,如果某个客户端的请求包很大(例如 SET 一个 10MB 的 value),读取数据会阻塞主线程,影响其他客户端。
4.1 连接流程:

- 首先在redis启动初始化的时候,redis中的I/O多路复用程序会监听serverSocket并且将事件处理器中的连接应答处理器和AE_READABLE事件关联起来。
- 当客户端跟redis发起连接时,serverSocket会产生一个AE_READABLE事件,然后被I/O多路复用程序监听到后传递给文件事件分派器。
- 文件事件分派器再分配给redis初始化时与AE_READABLE事件关联的连接应答处理器去处理。
- 连接应答处理器完成与客户端的连接后,会创建一个与客户端一 一对应的socket,同时将这个socket的AE_READABLE事件跟命令请求处理器关联起来,后续该客户端的所有请求都是在这个socket上进行操作。我们可以把这个新建的socket叫作socket01,以便与其他客户端连接成功生成的socket进行区分,不同的客户端与serverSocket完成连接后都会创建一个与之一一对应的socket。
4.2 命令执行流程:

- 客户端发送一个set key value命令到socket01。
- socket01会产生一个AE_READABLE(读事件)事件。
- I/O多路复用程序监听到socket产生的AE_READABLE事件。
- I/O多路复用程序将该AE_READABLE事件放入到队列中。
- 文件事件分派器读取队列中的AE_READABLE事件,并从客户端socket01中读取请求数据交由命令请求处理器去处理。
- 命令请求处理器解析为具体的命令(如 GET、SET 等)并调用相应的命令处理函数执行命令,命令执行过程中,Redis 会操作内存中的数据(如读取或修改键值对)。
- 命令执行完成后,Redis 会将结果数据写入客户端socket01中,并将socket01的AE_WRITABLE事件和命令回复处理器关联起来。
- 当客户端准备接收redis返回的数据时,会产生一个AE_WRITABLE事件,该事件被I/O多路复用程序监听到后放入队列中,再由文件事件分派器分配给该事件关联的命令回复处理器去处理。
- 最后由命令回复处理器将结果发送给客户端后会删除这个socket01的AE_WRITABLE事件和命令回复处理器的关联。
五、I/O 多路复用机制详解
5.1 为什么需要多路复用?
单线程处理多个连接时,如果阻塞在某个 socket 的 read/write,其他连接就无法服务。多路复用让 Redis 可以同时监听多个 socket,只在有数据时才去读取,避免无谓等待。
5.2 底层实现抽象
Redis 对多路复用进行了封装,提供了统一的接口:
// ae.c 中的核心事件循环
int aeProcessEvents(aeEventLoop *eventLoop) {
// 调用多路复用 API 等待事件
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
// 处理每个就绪的事件
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
if (fe->mask & AE_READABLE) fe->rfileProc(...);
if (fe->mask & AE_WRITABLE) fe->wfileProc(...);
}
}
不同平台下的实现:
- Linux:epoll
- BSD/macOS:kqueue
- Solaris:evport
- 其他(旧系统):select
Redis 在编译时会自动选择最高效的可用机制。
六、单线程模型的优缺点
6.1 优点:
-
避免锁和并发竞争,实现天然原子性
- 无锁设计:所有命令在单线程中串行执行,无需对数据结构加锁(如哈希表、跳表)。
- 原子操作:单个命令天然是原子的,无需 MULTI/EXEC 就能保证并发安全。
-
避免上下文切换和 CPU 缓存失效
- 无线程切换开销:单线程不会因为时间片耗尽而被动切换,也没有线程创建/销毁成本。
- CPU 缓存友好:数据结构和热点变量一直停留在 L1/L2 缓存中,不会因线程切换导致缓存失效。
- 可预测的性能:单线程的性能曲线是平滑的,不会因为线程数增加而出现性能抖动。
-
简单且可维护
- 易于调试:所有操作顺序执行,没有竞态条件、死锁、数据竞争等并发 Bug。
- 实现简洁:事件循环、命令表、数据结构都可以用最简单的同步方式编写。
-
对内存数据结构的极致优化
- 单线程使得 Redis 可以使用复杂但高效的数据结构(如跳表、压缩列表),而不必担心并发修改。
-
组合操作的原子性
- 虽然 Redis 不支持多命令的事务回滚,但通过 Lua 脚本 可以在单线程中原子执行一串命令。
- 单线程保证了 Lua 脚本执行期间,不会有其他命令插入,实现复杂的原子更新(如扣库存、转账)。
6.2 缺点:
-
单个慢命令阻塞整个服务
- 这是最严重的缺陷。因为所有命令串行执行,任何一个耗时操作都会导致后续所有请求排队等待。
- 典型案例:
- KEYS *:扫描全库,千万级 key 可能耗时数秒。
- HGETALL 大 Hash:读取百万字段的 Hash,耗时数百毫秒。
- ZRANGE 大 ZSet:返回大量数据,序列化和传输耗时。
- SORT 复杂排序:O(N log N) 操作,数据量大时阻塞。
- Lua 脚本死循环或长时间循环:彻底卡死 Redis。
- 后果:
- 其他客户端请求超时。
- 哨兵可能误判节点下线(PING 不响应)。
- 集群节点可能被标记为 FAIL。
- 解决方案:
- 使用 SCAN、HSCAN、SSCAN、ZSCAN 代替全量遍历。
- 对大 key 进行拆分(如将大 Hash 拆成多个小 Hash)。
- 使用 UNLINK 异步删除大 key。
- 监控慢查询(slowlog get),设置合理阈值(如 10ms)。
- Lua 脚本中避免循环操作大量数据。
-
无法充分利用多核 CPU
- 单线程只能使用一个 CPU 核心,即使服务器有 64 核,Redis 也只能跑满一个核。
- 对于 CPU 密集型操作(如大量 SINTER、ZUNIONSTORE),单核成为瓶颈。
- 解决方案:
- 部署多个 Redis 实例:一台物理机部署多个 Redis 进程(如 8 核部署 8 个实例),使用端口区分,业务层进行分片(客户端一致性哈希或 Redis Cluster)。
- 使用 Redis Cluster:将数据分散到多个 Master 节点,每个 Master 使用一个核心。
- 多线程 I/O(Redis 6.0+):可提升网络吞吐,但不能解决命令执行阶段的 CPU 瓶颈。
-
大对象操作的内存释放阻塞
- 删除一个包含百万元素的 Hash 或 List 时,DEL 命令会逐个释放内存,耗时较长(O(N))。
- 在单线程中,释放过程会阻塞事件循环。
- 解决方案:
- 使用 UNLINK 代替 DEL:Redis 4.0+ 支持,将实际释放操作交给后台线程,主线程立即返回。
-
网络吞吐量受限于单核处理能力
- 即使命令执行很快(如 PING、GET 小 key),网络读写(解析 RESP 协议、分配缓冲区)也会消耗 CPU。
- 当 QPS 超过 10 万时,单核可能满载,网络包处理成为瓶颈。
- 解决方案:
- Redis 6.0 多线程 I/O:将网络读写分给多个 I/O 线程,主线程只负责命令执行,可提升 2~3 倍吞吐量。
- 批量操作(Pipeline)减少网络往返。
七、Redis 6.0+:多线程 I/O(核心变化)
7.1 设计目标
将网络 I/O 读写从主线程中分离出去,由多个 I/O 线程并行处理,而命令执行仍然在主线程串行。这样既保证了原子性,又提升了网络吞吐量。
7.2 整体架构图
┌─────────────────────────────────────────────────────────┐
│ 主线程 │
│ - 接收新连接 │
│ - 事件循环 (aeMain) │
│ - 命令执行 (processCommand) │
│ - 将请求分配给 I/O 线程 │
│ - 收集 I/O 线程的解析结果 │
└──────────────┬──────────────────────┬───────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ I/O 线程 1 │ │ I/O 线程 2 │
│ - 从 socket 读取 │ │ - 从 socket 读取 │
│ - 解析 RESP 协议 │ │ - 解析 RESP 协议 │
│ - 写回响应数据 │ │ - 写回响应数据 │
└──────────────────┘ └──────────────────┘
▲ ▲
└──────────┬───────────┘
│
┌──────┴──────┐
│ I/O 线程 N │
└─────────────┘
7.3 工作流程
阶段 1:请求读取(多线程读)
传统单线程方式:
epoll_wait 返回 → 主线程循环 read() + 解析 → 执行命令
多线程读方式:
epoll_wait 返回 → 主线程将就绪的客户端分配给 I/O 线程 →
I/O 线程并行 read() + 解析 → 主线程从队列取出已解析的命令 → 执行
核心步骤:
- 事件循环:主线程调用 aeApiPoll 获取就绪的客户端 fd。
- 分发:主线程将就绪的客户端轮询分配给各个 I/O 线程(存入 io_threads[id].clients_pending)。
- 唤醒:主线程设置标志,唤醒 I/O 线程。
- 并行读取:每个 I/O 线程循环处理自己队列中的客户端:
- 调用 read() 从 socket 读取数据。
- 解析 RESP 协议,将命令存入 client->querybuf。
- 标记命令已解析完成。
- 收集:主线程等待所有 I/O 线程完成,然后从每个客户端取出命令执行。
- 执行:主线程串行执行所有命令(这一步仍然是单线程)。
阶段 2:响应写入(多线程写)
执行完命令后,响应数据被写入客户端的输出缓冲区。发送响应时:
传统单线程方式:
主线程直接 write() 或注册写事件后主线程发送
多线程写方式:
主线程执行命令 → 响应入队 → 将客户端分配给 I/O 线程 →
I/O 线程并行 write() 发送数据
核心步骤:
- 命令执行:主线程执行命令,将响应写入 client->buf 或 client->reply。
- 入队:将客户端加入 server.clients_pending_write 队列。
- 分发:在 beforeSleep() 中,主线程将该队列的客户端分配给 I/O 线程。
- 并行写入:I/O 线程调用 write() 将数据发送给客户端。
- 清理:发送完成后,如果还有数据未发完,重新注册写事件(交给主线程或下次 I/O 线程)。
7.4 配置与启用
Redis 6.0 默认关闭多线程 I/O,需要手动配置。 什么时候应该开启?
- 当前 Redis QPS > 5 万,且 CPU 使用率接近单核 100%。
- 网络 I/O 占比高(用 perf top 看到 read/write 系统调用占用高)。
- value 较大(>1KB)或 Pipeline 批量请求多。
开启步骤:
-
修改 redis.conf
# redis.conf # 1. 开启多线程读(默认 no) io-threads-do-reads yes # 2. 设置 I/O 线程数量(默认 4) io-threads 4 # 3. 相关优化:避免线程频繁创建销毁 # (无需额外配置,Redis 启动时创建线程池)- io-threads-do-reads 设为 yes 后才开启读取多线程;否则仅写入多线程。
- 线程数并非越大越好,通常设为 4-8 即可,或者为:CPU核心数 / 2,最多不超过 8-10。
-
重启 Redis
redis-server redis.conf -
验证
redis-cli INFO server | grep io_threads # 输出:io_threads_active:1
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)