Skip to content

线程模型 ​

Redis 为什么快,最常被提到的答案是「单线程」。但这个说法只对了一半——命令执行是单线程,网络 IO 在 6.0 之后却可以多线程。本章拆解 Redis 的线程模型与 IO 多路复用机制,把「为什么快」讲清楚。

1. 单线程模型 ​

Redis 的核心是单线程执行命令:同一时刻只有一个命令在执行,命令之间天然串行,不存在线程切换、锁竞争、上下文切换的开销。

txt
客户端 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 采用「注册 + 事件通知」模型:

txt
epoll_create  创建 epoll 实例
epoll_ctl     注册需要监听的 fd(一次注册,长期有效)
epoll_wait    阻塞等待就绪事件(内核直接返回就绪 fd 列表)

核心改进:

能力select/pollepoll
获取就绪 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 多路复用做了统一封装:

c
// 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():

c
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 模式——一个事件分发器 + 多个事件处理器。

事件循环的工作流程:

  1. epoll_wait 阻塞等待就绪事件
  2. 根据事件类型(可读 / 可写)分发到对应处理器
  3. 可读事件 → 读取请求、解析命令、执行、准备响应
  4. 可写事件 → 把响应写回客户端

三类事件:

事件触发时机处理
连接事件新客户端建立连接接受连接、初始化客户端
读事件客户端发来命令读取并解析命令
写事件响应可写回把结果写回客户端

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 线程并行处理,而命令的真正执行仍在主线程串行:

txt
主线程:读事件分发 → 多线程解析请求 → 主线程执行命令 → 多线程写回响应
              │              ▲                  │                ▲
              └── IO线程1 ────┘                  └─── IO线程1 ────┘
              └── IO线程2 ────┘                  └─── IO线程2 ────┘

关键点:

环节线程说明
协议解析多线程把网络字节流解析成命令
命令执行单线程保证原子性,无锁
响应写回多线程把结果写回客户端

5.3 配置 ​

bash
io-threads 4                  # IO 线程数(默认 1 = 关闭多线程)
io-threads-do-reads yes       # 是否开启多线程读

只有当 CPU 核数较多(≥ 4 核)且确实存在网络 IO 瓶颈时才建议开启多线程 IO。核数少或瓶颈在命令执行时,开启多线程反而可能因线程切换降低性能。

6. 小结 ​

要点说明
命令执行单线程,保证原子性,无锁
网络 IO6.0 起支持多线程,解析和写回并行
IO 多路复用epoll/kqueue,一个线程监听万级连接
慢命令阻塞所有请求,是单线程模型的最大风险
辅助线程AOF 刷盘、UNLINK、Lazy Free 不阻塞主线程
Reactor 模式事件驱动,serverCron 处理定时任务

Released under the MIT License.