Redis单线程事件循环IO多路复用架构

在探讨 Redis 的并发模型前,必须明确其所处的物理环境:这是一个 纯内存数据库

现代 CPU 访问一次一级缓存(L1 Cache)的时间约 1 纳秒,访问一次主存(RAM)的时间约 100 纳秒。而一次同城机房的千兆网络 RTT(往返延迟)大约在 500 微秒(500,000 纳秒)。

这意味着,CPU 计算和内存读写的速度,与网络报文到达网卡的速度之间,存在数千倍的鸿沟。

因此对于 Redis 而言,瓶颈不在 CPU 的运算频率,而在网络带宽的吞吐量,以及系统调用的开销

为了在单线程模型下突破网络处理的限制,Redis 构建了 基于 I/O 多路复用的事件驱动架构

I/O 多路复用与 Reactor 事件循环

为了应对海量并发连接,Redis 摒弃了传统数据库(如 MySQL)“一连接一线程”的模型,通过操作系统内核提供的 I/O 多路复用原语,构建了极简的 事件驱动抽象层(aeEventLoop),即 Reactor 模式。

ae 这个抽象层可以屏蔽底层 OS 差异,它会在编译时自动选择最佳方案:

  • Linux: epoll
  • macOS/FreeBSD: kqueue
  • Windows/Legacy: select

多路复用的核心在于:利用单一线程同时监控多个网络连接(文件描述符 File Descriptor, FD)的状态,仅当 FD 真正就绪时,才触发处理逻辑

在 Linux 环境下,Redis 将复杂的网络生命周期降维为 epoll 的三个核心系统调用,其串行事件循环逻辑如下:

  1. epoll_create:在内核态申请一块内存,建立一棵红黑树(用于维护所有注册的 TCP 连接)和一个双向就绪链表。
  2. epoll_ctl:新客户端连入时,将对应的 Socket 注册至红黑树,并 向底层网卡驱动注册硬中断回调
  3. epoll_wait:主线程在此挂起并让出 CPU。当网卡接收到数据并触发硬件中断后,内核将对应的 Socket 推入就绪链表,随后唤醒主线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 伪代码:Redis 核心事件循环抽象 (aeMain)
void aeMain(aeEventLoop *eventLoop) {
while (!eventLoop->stop) {
// 1. 阻塞等待:调用底层 epoll_wait,线程休眠,等待内核唤醒
int numEvents = aeApiPoll(eventLoop, timeval);

// 2. 串行处理所有就绪的网络事件
for (int i = 0; i < numEvents; i++) {
aeFileEvent *fe = &eventLoop->events[fd];
if (fe->mask & AE_READABLE) fe->rfileProc(fd); // 读取、解析与命令执行
if (fe->mask & AE_WRITABLE) fe->wfileProc(fd); // 响应回写
}
}
}

坚持单线程执行的底层逻辑

引入多线程执行命令,必然引入 并发控制(锁机制)。对于 Hash、ZSet 这种极其复杂的内存数据结构,加锁会导致极其高昂的代价:

  1. 系统态阻塞:无论是 Mutex 还是 Spinlock,高并发下的锁竞争都会导致线程被迫让出 CPU,引发密集的上下文切换(寄存器保存、TLB 快表失效)。
  2. 缓存行颠簸(Cache Line Bouncing):多核 CPU 架构下,多线程频繁修改共享内存,会导致 CPU 内部的 MESI(缓存一致性协议)频繁触发。L1/L2 缓存中的数据必须不断失效与通过总线同步,内存的读写性能将急剧退化。

Redis 选择单线程,不仅彻底免除了锁的开销,更保证了单条命令(或 Lua 脚本)在执行时的 绝对物理原子性

Redis 的单线程仅指 核心命令执行与网络 I/O 调度 由主线程完成。对于持久化、异步删除(UNLINK)、集群同步等耗时操作,Redis 在架构侧将其深度异步化,交由后台 bio 线程池或独立的子进程执行。

架构命门:队头阻塞

单线程事件循环机制获取了极致的缓存局部性,但也确立了其致命的物理命门:队头阻塞(Head-of-Line Blocking)

在单线程模型中,时间轴是绝对一维的**。如果主线程正在处理一条耗时极长的指令,底层的 epoll_wait 将无法被及时调用。此时网卡接收到的数据无法被搬运,内核层的 TCP 全连接队列(Accept Queue)会迅速堆满,最终导致操作系统向后续客户端返回 Connection Refused 或引发大面积超时**。

因此,Redis 架构的防御底线在于:严格隔离任何时间复杂度为 O(N) 的长耗时操作入侵主线程

  • 业务侧约束:严禁在生产环境执行 KEYS *、无边界的 HGETALL 或使用 DEL 删除包含百万元素的 Big Key。
  • 底层机制隔离:如生成 RDB 快照时,Redis 通过 fork 系统调用利用操作系统的写时复制机制,由独立子进程完成内存转储,绝对不侵占主线程的执行周期。

物理瓶颈转移——Redis 6.0 多线程 I/O 演进

在 10Gbps+ 甚至更高速的现代万兆网络环境下,系统的物理瓶颈再次发生转移。

当每秒需要处理上百万个网络包时,CPU 的时间不再消耗于“执行 Redis 内存计算”,而是被 海量的系统调用和内存拷贝 压垮。主线程每一次调用 read() 或 write() 搬运网络数据,都要求 CPU 触发 int 0x80 中断,完成从 Ring 3 用户态到 Ring 0 内核态的陷入与切出;同时,将字节流解析为 RESP 协议的过程也消耗了大量 CPU 周期。单线程在处理网络读写及协议解析时,往往会占用到 70% 以上的 CPU 资源。

为解决这一网络 I/O 瓶颈,Redis 6.0 引入了 I/O 多线程机制。其核心妥协极其精准:多线程仅接管 Socket 的读写与协议解析,真正的命令执行环节依然由主线程串行处理

Redis 6.0 混合并发工作流程:

  1. 分发读取:主线程在事件循环中唤醒,将一批就绪的客户端 Socket 分配给 I/O 线程组。
  2. 并行读与解析:I/O 线程组多核并行地执行 read(),从 Socket 读取数据并将字节流解析为 RESP 命令(不执行具体的 Redis 指令)。
  3. 同步等待:主线程阻塞,等待所有 I/O 线程完成读取与解析操作。
  4. 串行执行:主线程 单线程串行 执行所有已解析完毕的命令。
  5. 分发与并行写入:命令执行完毕后,主线程将生成的响应数据分发给 I/O 线程组,由多线程并行调用 write() 将数据写回 Socket。

通过这种半同步半异步的混合设计,Redis 既成功卸载了系统调用与内存拷贝的 CPU 压力,又完整保留了单线程操作复杂内存数据结构的无锁优势,最终在网络层面上实现了物理延迟边界内的极限吞吐。


Redis单线程事件循环IO多路复用架构
https://gavinmo1.github.io/2025/12/02/Redis单线程事件循环IO多路复用架构/
作者
Gavin
发布于
2025年12月2日
许可协议