架构、事件循环与线程模型
→ 返回 Redis
Redis 的“单线程”特指:一条命令对内存数据的核心执行过程主要由主线程串行完成。它不等于整个进程只有一个线程:后台删除、AOF 刷盘、关闭文件、RDB/AOF 子进程,以及 Redis 6.0+ 的网络 I/O 线程都会并行工作。
Redis 服务端没有一个把普通
GET/SET分发给多个 worker 并行修改键空间的“通用命令线程池”。实际存在的是职责分离的 I/O 线程、后台 I/O(BIO)工作线程和持久化子进程;Java 客户端的 Netty 线程池/连接池又是另一层。
客户端 Socket
│ 连接/读/写就绪
▼
ae 事件循环(Linux 通常选 epoll)
│
├─ 文件事件:accept、读取请求、写回响应
├─ 时间事件:serverCron、过期采样、统计、集群心跳
└─ beforeSleep / afterSleep:集中处理队列与 I/O
│
▼
主线程解析并串行执行命令
│
├─ 内存对象与字典
├─ AOF / 复制传播
└─ 响应缓冲区为什么核心命令使用单线程
| 收益 | 含义 |
|---|---|
| 避免锁竞争 | 修改字典和对象时不需要为每条命令加锁 |
| 命令天然串行 | 单条命令执行期间不会被另一条普通命令穿插,便于保证原子性 |
| CPU 缓存友好 | 少了线程切换和共享状态同步 |
| 实现简单 | 网络并发与数据执行解耦,错误边界更清晰 |
Redis 快的核心是内存访问 + 高效数据结构 + 非阻塞 I/O + 少量系统调用,不能简单归因于“单线程”。当命令本身为 O(N)、访问大 key、执行复杂 Lua,主线程仍会被阻塞。
常见阻塞源:
KEYS *、大范围HGETALL/SMEMBERS、大集合运算;- 删除超大 key(优先
UNLINK,让后台线程回收对象); - Lua / Function 长时间运行;
fork时内存页表复制,以及写时复制(COW)造成的内存和延迟抖动;- 磁盘慢导致 AOF
fsync或后台重写压力传导。
epoll 与 Reactor 事件循环
Redis 自己实现了轻量事件库 ae,按平台选择后端:Linux 通常是 epoll,BSD/macOS 通常是 kqueue,其他平台可退化为 poll / select。因此更准确的说法是“Redis 使用 I/O 多路复用”,而不是所有系统都固定使用 epoll。
一次读请求如何执行
- Redis 将监听 socket 和客户端 socket 注册为文件事件,并设置为非阻塞。
- 主线程调用事件轮询函数;没有就绪事件时线程可以休眠,不必逐个 socket 空转查询。
- epoll 返回已就绪的 fd,Redis 调用对应回调:接受连接或读取请求。
- 请求进入输入缓冲区,解析 RESP 协议,生成待执行命令。
- 主线程查询命令表、校验参数与权限,串行操作内存数据。
- 响应先写入客户端输出缓冲区;若一次未写完,则注册可写事件,等待下次就绪后继续发送。
这是典型的 Reactor:事件分发器报告“哪个 fd 可以读写”,真正的读取、命令执行和响应处理由 Redis 回调完成。epoll 不负责执行业务,也不会让一条 Redis 命令自动并行。
epoll 为什么适合大量连接
- 一个线程可以等待大量 fd,无须“一连接一线程”;
- 只返回当前就绪事件,避免每轮线性扫描全部连接;
- socket 非阻塞,单个慢客户端不会因一次读写直接卡住整个循环;
- 事件注册与就绪集合分离,减少无效系统调用。
Redis 通常使用水平触发语义,并在回调中尽量读完当前可读数据。应用层仍需限制客户端输入/输出缓冲区,否则慢消费者可能占用大量内存。
Redis 6.0+ I/O 多线程
I/O 多线程解决的是 socket 读写、响应发送等网络阶段在高吞吐下占用 CPU 的问题,开启 threaded reads 后也可并行处理读取阶段;命令对键空间的核心执行仍由主线程串行完成。
主线程等待就绪事件
├─ I/O 线程并行读取(开启 io-threads-do-reads 后)
├─ 主线程串行执行命令
└─ I/O 线程并行写响应io-threads 4
io-threads-do-reads yesio-threads 1 等价于不启用额外 I/O worker。只配置 io-threads > 1 时,主要并行响应写出;io-threads-do-reads yes 才把读取阶段也交给 I/O 线程。具体执行阶段会随 Redis 版本演进,应以当前版本配置说明和压测为准。
一批命令的阶段屏障
简化理解如下:
主线程收集待处理客户端
│
├─ 分配给 I/O threads 并行读/写网络缓冲区
│ ▼
└──────── 等待本批 I/O 完成
▼
主线程串行执行命令
▼
下一批响应并行写出I/O 线程不是让 INCR、Lua、HGETALL 在多个 CPU 核上同时执行。慢命令仍会卡住主线程,所有 I/O worker 最终也要等待命令执行阶段。
什么时候值得开启
| 现象 | 判断 |
|---|---|
| 单核主线程接近饱和,网络收发/协议处理占比高 | I/O threads 可能有效 |
| 大量小命令、高连接数、较高网络吞吐 | 值得以 2/4 等小步压测 |
| 慢 Lua、大 key、大集合运算导致延迟 | 加 I/O 线程基本无效,应先治理命令 |
| 单实例 QPS 不高、CPU 仍有余量 | 保持默认通常更简单 |
| 容器 CPU quota 很小或发生 throttling | 线程过多可能互相争抢,尾延迟更差 |
线程数不是越多越好:主线程仍是串行瓶颈,I/O worker 还会增加调度、同步、缓存竞争和内存开销。不要直接照搬“CPU 核数 × 2”;从较小值开始,在相同请求大小、pipeline、连接数和持久化配置下比较吞吐与 p99/p999。
CPU 规划
- 为主线程保留稳定 CPU 时间,不要让 I/O worker、AOF/RDB 和同机业务争抢同一小份 quota;
- Redis Cluster 是多进程/多分片扩展,单机多个实例时需要把所有实例的主线程、I/O 线程和后台任务一起预算;
- NUMA、容器限核和云主机 steal/throttling 都会让“宿主机有很多核”失去意义;
- CPU affinity 是高级优化,应先解决慢命令、大 key、网络和内存瓶颈。
BIO 后台工作线程
Redis 的 bio(Background I/O)将少量可能阻塞主线程的系统操作放入后台任务队列。它是按职责划分的内部 worker,并不是普通 Redis 命令线程池。
| 后台职责 | 典型触发 | 注意点 |
|---|---|---|
AOF fsync | appendfsync everysec 等 | 磁盘卡顿会形成待处理压力并影响尾延迟 |
| 异步关闭文件 | RDB/AOF 文件切换 | 避免某些 close 操作阻塞主循环 |
| Lazy Free | UNLINK、FLUSHDB ASYNC、配置的异步驱逐/过期 | 主线程摘除引用,后台遍历并释放对象 |
Lazy Free 不等于删除零成本
UNLINK big-key
├─ 主线程:从键空间摘除,命令快速返回
└─ BIO:递归释放对象和内存后台释放仍消耗 CPU、内存带宽和 allocator 时间。短时间 UNLINK 大量超大 key 可能让 lazy-free 队列积压;主线程虽然不再长时间停顿,整机资源仍可能被拖慢。
相关配置会随版本变化,常见方向包括:
lazyfree-lazy-user-del yes
lazyfree-lazy-user-flush yes
lazyfree-lazy-expire yes
lazyfree-lazy-eviction yes
replica-lazy-flush yes是否开启要结合内存峰值:对象从字典移除后,在后台真正释放前仍占用物理内存。
子进程不是线程池
| 执行单元 | 典型职责 | 与主进程关系 |
|---|---|---|
| RDB 子进程 | BGSAVE 生成快照 | fork 后读取快照视图 |
| AOF 重写子进程 | BGREWRITEAOF 生成紧凑 base | fork + COW,父进程继续服务 |
RDB/AOF 使用 fork 后,父子进程初始共享物理内存页;父进程继续写数据时触发 COW。它们不会从队列中取普通客户端命令执行,也不属于可调大小的 worker pool。
数据集越大、页表越大,fork 停顿风险越高;子进程运行期间写入越密集,COW 额外内存越多。应监控 latest_fork_usec、rdb_last_cow_size、aof_last_cow_size 和机器可用内存。
服务端线程与客户端线程不要混淆
应用 Tomcat/虚拟线程
→ Lettuce/Redisson Netty EventLoop
→ TCP 连接(可能复用/池化)
→ Redis I/O threads
→ Redis 主线程执行命令增加应用线程、Netty 线程或连接数,只会提高并发提交能力;它不会让单个 Redis 分片的键空间执行自动并行。上游无限并发还会造成:客户端待发送队列、连接池等待、Redis 输入/输出缓冲区和超时重试同时膨胀。
客户端线程和连接池详见 线程池与客户端连接。
监控与诊断
INFO threads # Redis 8.0+ I/O thread 分配、read/write 统计
INFO cpu
INFO clients
INFO commandstats
INFO latencystats
LATENCY DOCTOR
SLOWLOG GET 20| 指标/现象 | 可能原因 |
|---|---|
| 主线程 CPU 高、网络吞吐高、无明显慢命令 | 评估 I/O threads 或水平分片 |
| p99 高且 slowlog 有大命令 | 治理命令/大 key,线程数不是根因 |
blocked_clients 高 | BLPOP/XREAD 等阻塞命令或模块阻塞;检查用途是否符合预期 |
| AOF 延迟与磁盘 await 同时升高 | fsync/重写 I/O 竞争 |
| used_memory 暂不下降、lazy free 活跃 | 后台释放尚未完成或 allocator 未归还 OS |
| 客户端超时但 Redis CPU 不高 | 网络、客户端 EventLoop 被阻塞、池耗尽或超时配置 |
面试结论
Redis 没有通用命令执行线程池:主线程通过 ae/epoll 处理事件并串行修改键空间;Redis 6.0+ I/O threads 只并行网络读写阶段,BIO 线程处理 fsync、close、lazy free,RDB/AOF 重写使用子进程。调大服务端 I/O 线程或客户端连接池都不能解决慢命令和大 key,必须结合 CPU、网络、延迟和队列指标压测。
相关
- 底层数据结构与对象编码
- 线程池与客户端连接
- 过期与持久化
- 运维与集成(慢查询、大 key)