Redis 为什么要用单线程?
很多人在第一次听到「Redis 是单线程」时,第一反应是:这东西能快吗?
毕竟我们习惯了多线程并发处理请求,一核干活总比不过八核一起干吧?
但事实是:Redis 单线程不仅快,而且快得离谱——QPS 可以达到 10 万+,碾压大多数多线程系统。
这是怎么回事?
单线程,到底指的是什么?
首先澄清一个常见误解:Redis 单线程指的是网络 I/O 和命令执行,而不是整个进程只有一个线程。
Redis 服务端主进程确实是单线程处理命令,但背后还有:
- 后台线程:负责持久化(AOF 刷盘、RDB 子进程)、懒删除、关闭文件等
- AOF 刷盘线程:异步将 AOF 缓冲区刷到磁盘
- bio 线程:Linux 提供的阻塞 I/O 线程池
所以 Redis 的线程模型大致是这样的:
┌─────────────────────────────────────────┐
│ 主线程(单线程) │
│ - 接收连接 │
│ - 读取命令 │
│ - 执行命令 │
│ - 写回响应 │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 后台线程(多线程) │
│ - 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 | 高效的事件通知,只返回就绪的 FD | Redis 6.0 前主要使用 |
epoll 的核心优势:不用遍历所有连接,只处理活跃的连接。
// 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 和步骤 4 涉及大量网络 I/O,是可以并行的。于是 Redis 6.0 将这两步拆分到 I/O 线程池:
┌─────────────────────────────────────────────────┐
│ 主线程 │
│ - 解析命令 │
│ - 执行命令 │
└─────────────────────────────────────────────────┘
│ ▲
▼ │
┌─────────────────┐ ┌─────────────────┐
│ I/O 线程池 │ │ I/O 线程池 │
│ (读取命令) │ ───▶ │ (写回响应) │
└─────────────────┘ └─────────────────┘注意:命令执行仍然是单线程的,这是 Redis 高性能的根本保证。
单线程的代价
单线程也不是银弹,它有自己的局限性:
- 无法利用多核 CPU:一台 64 核的服务器,Redis 只能用 1 核
- 阻塞命令会卡住整个服务:如
FLUSHDB、KEYS *、大的SORT操作 - QPS 有上限:单机 QPS 大约 10-20 万
解决方案是多实例部署:
# 启动多个 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 命令。
