线程模型
Redis 为什么快,最常被提到的答案是「单线程」。但这个说法只对了一半——命令执行是单线程,网络 IO 在 6.0 之后却可以多线程。本章拆解 Redis 的线程模型与 IO 多路复用机制,把「为什么快」讲清楚。
1. 单线程模型
Redis 的核心是单线程执行命令:同一时刻只有一个命令在执行,命令之间天然串行,不存在线程切换、锁竞争、上下文切换的开销。
客户端 A ──┐
客户端 B ──┤──► 事件循环(单线程)──► 逐个执行命令
客户端 C ──┘单线程为什么反而快:
| 因素 | 说明 |
|---|---|
| 纯内存操作 | 数据都在内存,多数场景瓶颈在内存与网络,而非 CPU 计算 |
| 无锁无切换 | 单线程避免命令执行路径上的线程切换与锁竞争开销 |
| IO 多路复用 | 一个线程通过 epoll 同时监听成千上万个连接 |
| 高效数据结构 | SDS、跳表、字典等,多数操作 O(1)/O(log n) |
单线程的边界:单线程意味着慢命令会阻塞所有其他请求。一个耗时 1 秒的命令,会让事件循环阻塞 1 秒,期间其他命令无法执行。所以 Redis 的优化核心是「避免慢命令」(见 命令与 RESP 与 性能)。
辅助线程:虽然命令执行是单线程,但 Redis 有多个后台线程处理耗时操作——AOF 刷盘、UNLINK 异步删除大 Key、Lazy Free 惰性释放,这些不阻塞命令执行。
2. 命令执行时间线
一条命令从发出到返回的完整时间线:
3. IO 多路复用
IO 多路复用让一个线程同时监听多个网络连接。它的演进是从「轮询」到「事件通知」。
3.1 select / poll 的局限
早期的 select 和 poll 每次调用都要把监听的所有文件描述符(fd)从用户态拷贝到内核态,内核再遍历全部 fd 找出就绪的,时间复杂度 O(n)。连接数一多,性能急剧下降。
3.2 epoll 的改进
Linux 的 epoll 采用「注册 + 事件通知」模型:
epoll_create 创建 epoll 实例
epoll_ctl 注册需要监听的 fd(一次注册,长期有效)
epoll_wait 阻塞等待就绪事件(内核直接返回就绪 fd 列表)核心改进:
| 能力 | select/poll | epoll |
|---|---|---|
| 获取就绪 fd | 每次遍历全部 fd,O(n) | 内核维护就绪列表,直接返回 |
| 用户态拷贝 | 每次拷贝全部 fd | 只需处理就绪的 fd |
| 连接数上限 | select 受 FD_SETSIZE 限制,常见默认为 1024;poll 无固定位图限制但需遍历 | 受系统文件描述符和内存限制,不遍历全部已注册 fd |
Redis 使用 epoll(Linux)/ kqueue(macOS)/ evport(Solaris)等平台最优的多路复用实现,封装在统一的事件循环里。
3.3 ae 事件库
Redis 自己封装了一个轻量级事件库 ae,对不同平台的 IO 多路复用做了统一封装:
// ae.c 核心结构
typedef struct aeEventLoop {
int maxfd; // 当前最大 fd
int setsize; // 事件表容量(可追踪的最大 fd 数)
aeFileEvent *events; // 注册的事件表(动态分配)
aeFiredEvent *fired; // 就绪事件表(动态分配)
aeTimeEvent *timeEventHead; // 定时事件链表
int stop;
void *apidata; // 平台特定数据(epoll/kqueue)
} aeEventLoop;事件循环主函数 aeMain():
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// aeProcessEvents 内部依次完成:
// 1. 调用 aeApiPoll(封装 epoll_wait)等待 IO 事件
// 2. 就绪事件分发到对应处理器
// 3. 处理到期的定时事件(过期删除、serverCron)
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}4. Reactor 模式
Redis 的事件处理采用 Reactor 模式——一个事件分发器 + 多个事件处理器。
事件循环的工作流程:
epoll_wait阻塞等待就绪事件- 根据事件类型(可读 / 可写)分发到对应处理器
- 可读事件 → 读取请求、解析命令、执行、准备响应
- 可写事件 → 把响应写回客户端
三类事件:
| 事件 | 触发时机 | 处理 |
|---|---|---|
| 连接事件 | 新客户端建立连接 | 接受连接、初始化客户端 |
| 读事件 | 客户端发来命令 | 读取并解析命令 |
| 写事件 | 响应可写回 | 把结果写回客户端 |
4.1 serverCron 定时任务
除了 IO 事件,Redis 还有周期性执行的 serverCron;默认 hz=10 时周期约为 100ms,该值可配置:
| 任务 | 说明 |
|---|---|
| 过期键清理 | 定期删除策略(见 过期与淘汰) |
| RDB/AOF 检查 | 触发持久化条件判断 |
| 统计信息更新 | 内存、命中率、连接数等 |
| 集群心跳 | 集群模式下的 Gossip 消息 |
| 哨兵心跳 | 哨兵模式下的健康检查 |
5. 6.0 的多线程网络 IO
Redis 6.0 引入了多线程网络 IO,但命令执行仍然是单线程。
5.1 瓶颈在哪
单线程模型下,网络 IO 的读写(read/write)与协议解析同样要占用主线程的 CPU 时间。当连接数多、吞吐量大时,解析协议、写回响应会占满单核 CPU,成为瓶颈。
Redis 作者 antirez 在 2019 年公开表示「IO 多线程不会做,因为多数部署瓶颈在网络和内存,而非 CPU」,但随后的实测发现,高吞吐下
read/write系统调用与协议解析占用了单核 CPU 的大量时间。这是 6.0 引入多线程 IO 的重要动因。
5.2 改进方案
6.0 起,Redis 把「读请求解析」和「写响应」这两步交给多个 IO 线程并行处理,而命令的真正执行仍在主线程串行:
主线程:读事件分发 → 多线程解析请求 → 主线程执行命令 → 多线程写回响应
│ ▲ │ ▲
└── IO线程1 ────┘ └─── IO线程1 ────┘
└── IO线程2 ────┘ └─── IO线程2 ────┘关键点:
| 环节 | 线程 | 说明 |
|---|---|---|
| 协议解析 | 多线程 | 把网络字节流解析成命令 |
| 命令执行 | 单线程 | 保证原子性,无锁 |
| 响应写回 | 多线程 | 把结果写回客户端 |
5.3 配置
io-threads 4 # IO 线程数(默认 1 = 关闭多线程)
io-threads-do-reads yes # 是否开启多线程读只有当 CPU 核数较多(≥ 4 核)且确实存在网络 IO 瓶颈时才建议开启多线程 IO。核数少或瓶颈在命令执行时,开启多线程反而可能因线程切换降低性能。
6. 小结
| 要点 | 说明 |
|---|---|
| 命令执行 | 单线程,保证原子性,无锁 |
| 网络 IO | 6.0 起支持多线程,解析和写回并行 |
| IO 多路复用 | epoll/kqueue,一个线程监听万级连接 |
| 慢命令 | 阻塞所有请求,是单线程模型的最大风险 |
| 辅助线程 | AOF 刷盘、UNLINK、Lazy Free 不阻塞主线程 |
| Reactor 模式 | 事件驱动,serverCron 处理定时任务 |