Skip to content

Redis 为什么要用单线程?

很多人在第一次听到「Redis 是单线程」时,第一反应是:这东西能快吗?

毕竟我们习惯了多线程并发处理请求,一核干活总比不过八核一起干吧?

但事实是:Redis 单线程不仅快,而且快得离谱——QPS 可以达到 10 万+,碾压大多数多线程系统。

这是怎么回事?

单线程,到底指的是什么?

首先澄清一个常见误解:Redis 单线程指的是网络 I/O 和命令执行,而不是整个进程只有一个线程。

Redis 服务端主进程确实是单线程处理命令,但背后还有:

  • 后台线程:负责持久化(AOF 刷盘、RDB 子进程)、懒删除、关闭文件等
  • AOF 刷盘线程:异步将 AOF 缓冲区刷到磁盘
  • bio 线程:Linux 提供的阻塞 I/O 线程池

所以 Redis 的线程模型大致是这样的:

text
┌─────────────────────────────────────────┐
│              主线程(单线程)             │
│  - 接收连接                              │
│  - 读取命令                              │
│  - 执行命令                              │
│  - 写回响应                              │
└─────────────────────────────────────────┘


┌─────────────────────────────────────────┐
│            后台线程(多线程)             │
│  - AOF 刷盘线程                          │
│  - lazy free 线程                        │
│  - RDB/AOF rewrite 子进程                │
└─────────────────────────────────────────┘

为什么单线程反而更快?

这就涉及到计算机的性能瓶颈问题了。

1. 瓶颈不在 CPU,在 I/O

很多人觉得多线程快,是因为 CPU 可以并行处理。但对于大多数业务系统来说,真正的瓶颈是网络 I/O 和磁盘 I/O,而不是 CPU 计算。

一个命令的执行时间分布大致是这样的:

CPU 计算:        ██  (微秒级)
网络 I/O:      ████████████████  (毫秒级)
磁盘 I/O:    ████████████████████████████  (毫秒级,取决于磁盘类型)

当你的瓶颈在 I/O 时,多线程并不能解决问题,反而会引入额外的开销。

2. 避免了锁竞争

多线程系统最大的敌人是谁?

锁。

当多个线程访问共享资源时,需要加锁保护。锁的竞争会导致:

  • 线程等待,降低并发效率
  • 死锁风险
  • 上下文切换开销

Redis 单线程天然不存在锁竞争,所有命令顺序执行,数据结构也是线程安全的(因为只在单线程中访问)。

3. 内存操作,极致高效

Redis 的数据全部存在内存中,内存操作的速度是纳秒级的:

操作类型耗时
访问内存~100 纳秒
访问 SSD~100 微秒
访问机械硬盘~10 毫秒

在纳秒级的时间尺度上,CPU 和内存的配合已经足够高效,单线程顺序执行反而是最优解。

I/O 多路复用:一个人干多个人的活

单线程能处理高并发的秘密,在于 I/O 多路复用

传统的 I/O 模型是这样的:

阻塞 I/O:一个人排队打电话
         线程1 ──→ [打电话] ──→ 等待 ──→ [挂断]
         线程2 ──→ [打电话] ──→ 等待 ──→ [挂断]
         ...

多线程 I/O:多个人同时打电话
         线程1 ──→ [打电话]
         线程2 ──→ [打电话]
         ...

I/O 多路复用:一个人用交换机同时监控多个电话
         线程1 ──┐
         线程2 ──┼──→ [交换机] ──→ 监听多个连接 ──→ 处理就绪的
         线程3 ──┘

Linux 提供了三种 I/O 多路复用机制:

机制特点Redis 使用
select有 FD 数量限制(1024),需要遍历全部 FD早期使用
poll无 FD 数量限制,但仍需遍历不常用
epoll高效的事件通知,只返回就绪的 FDRedis 6.0 前主要使用

epoll 的核心优势:不用遍历所有连接,只处理活跃的连接

java
// epoll 工作原理的简化示意
while (true) {
    // 等待有 socket 活跃(阻塞)
    int readyCount = epoll_wait(epfd, events, MAX_EVENTS, timeout);
    
    // 只遍历就绪的 socket
    for (int i = 0; i < readyCount; i++) {
        int fd = events[i].fd;
        // 处理这个 socket 的请求
        handleRequest(fd);
    }
}

Redis 6.0:多线程的引入

说了这么多,为什么 Redis 6.0 又引入了多线程?

为了进一步提升网络 I/O 效率。

Redis 6.0 之前,主线程需要同时做:

  1. 读取客户端命令
  2. 解析命令
  3. 执行命令
  4. 写回响应

其中,步骤 1 和步骤 4 涉及大量网络 I/O,是可以并行的。于是 Redis 6.0 将这两步拆分到 I/O 线程池:

┌─────────────────────────────────────────────────┐
│                    主线程                         │
│   - 解析命令                                      │
│   - 执行命令                                      │
└─────────────────────────────────────────────────┘
         │                        ▲
         ▼                        │
┌─────────────────┐      ┌─────────────────┐
│  I/O 线程池     │      │  I/O 线程池     │
│  (读取命令)     │ ───▶ │  (写回响应)     │
└─────────────────┘      └─────────────────┘

注意:命令执行仍然是单线程的,这是 Redis 高性能的根本保证。

单线程的代价

单线程也不是银弹,它有自己的局限性:

  1. 无法利用多核 CPU:一台 64 核的服务器,Redis 只能用 1 核
  2. 阻塞命令会卡住整个服务:如 FLUSHDBKEYS *、大的 SORT 操作
  3. QPS 有上限:单机 QPS 大约 10-20 万

解决方案是多实例部署

bash
# 启动多个 Redis 实例,分别使用不同的 CPU 核心
redis-server --port 6379 --bind 0.0.0.0 --protected-mode no
redis-server --port 6380 --bind 0.0.0.0 --protected-mode no
redis-server --port 6381 --bind 0.0.0.0 --protected-mode no

配合 Nginx 或代理层做请求分发,就能充分利用多核 CPU。

总结

Redis 单线程为什么快?

原因说明
瓶颈不在 CPU真正的瓶颈是 I/O,单线程避免了锁竞争和上下文切换开销
内存操作纳秒级访问速度,CPU 计算不是瓶颈
I/O 多路复用epoll 高效监控大量连接,只处理活跃的
避免锁开销顺序执行,无锁竞争,数据结构天然线程安全

留给你的问题

Redis 6.0 引入了多线程 I/O,但命令执行仍然是单线程。

如果一个命令执行时间很长(比如 SCAN 遍历大量数据),会阻塞其他命令吗?Redis 是如何处理这种情况的?

提示:关注 lazy free 机制和 CLIENT PAUSE 命令。

基于 VitePress 构建