文章目录
Redis 之所以能够支撑每秒数万甚至数十万的 QPS,绝不仅仅是因为它是基于内存操作的。更深层次的原因在于其精妙的事件驱动模型和IO处理机制。
本文将深入 Redis 源码层面,拆解其核心运行机制(Engine),带你理解 AeEventLoop、Reactor 模式以及 Redis 线程模型的演进之路。
1. 核心基石:Redis 的事件模型 (AeEventLoop)
Redis 是一个事件驱动程序。服务器运行的本质就是一个死循环(Event Loop),在这个循环中不断地等待事件发生、处理事件。
1.1 IO 多路复用与 Reactor 模式
Redis 并没有为每个客户端连接创建一个线程(这是传统 BIO 模型的做法),而是利用了 IO 多路复用技术(IO Multiplexing)。
- 概念:单个线程通过记录跟踪每一个 Socket(I/O流)的状态来同时管理多个 I/O 流。
- 实现:在 Linux 下主要依赖
epoll,macOS 下依赖kqueue。Redis 封装了一套ae.c事件库来屏蔽底层操作系统的差异。 - 模式:这就是典型的 Reactor 模式。Redis 作为 Reactor(反应堆),当 Socket 可读或可写时,分发给相应的处理器(Handler)。
1.2 源码透视:ae.c 与 ae_epoll.c
在 Redis 源码中,aeEventLoop 结构体是整个事件循环的核心。
核心结构体 (ae.h):
typedef struct aeEventLoop {
int maxfd; /* 当前注册的最大文件描述符 */
int setsize; /* 关注的文件描述符上限 */
aeFileEvent *events; /* 注册的文件事件 */
aeFiredEvent *fired; /* 已触发的事件(被epoll唤醒后填充) */
// ... 省略时间事件等
void *apidata; /* 用于存储底层API的数据,如epoll_event结构 */
} aeEventLoop;
主循环逻辑 (ae.c):
Redis 的 main 函数最终会调用 aeMain,启动死循环。
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件循环前执行的函数(如 flushAppendOnlyFile)
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 核心:处理文件事件和时间事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}
epoll 的封装 (ae_epoll.c):
aeProcessEvents 内部会调用 aeApiPoll,即 epoll_wait。
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
// 调用操作系统的 epoll_wait,获取就绪的事件
// state->events 是 struct epoll_event 数组
int retval = epoll_wait(state->epfd, state->events, eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
// ... 将内核返回的就绪事件,填充到 eventLoop->fired 数组中
return retval;
}
2. 全景图解:一条命令的执行流程
理解了事件循环,我们来看看当客户端发送一个 SET key value 命令时,Redis 内部发生了什么。
2.1 执行步骤
- 建立连接:客户端发起连接,服务端
listenfd 触发读事件,调用acceptTcpHandler建立连接,并创建一个client对象,注册readQueryFromClient读事件处理器。 - 读取请求:当客户端发送数据,Socket 可读,触发
readQueryFromClient,将数据读入输入缓冲区(querybuf)。 - 协议解析:解析 RESP 通信协议,识别命令和参数。
- 命令查找:根据命令名称(如 “SET”)在命令表(Command Table)中查找对应的函数。
- 执行命令:调用对应的命令函数(如
setCommand),操作内存数据结构(dict)。 - 写入回复:将结果写入输出缓冲区(buf 或 reply 链表),并注册写事件(如果数据量大)。
- 发送结果:当 Socket 可写时,
sendReplyToClient将数据发送回网卡。
2.2 时序图演示
3. 线程模型的演进:从单线程到多线程 IO
“Redis 是单线程的”这句话在 Redis 6.0 之后不再完全准确。我们需要从历史演进的角度来看待这个问题。
3.1 Redis 4.0 之前:纯粹的单线程?
实际上,Redis 一直都有后台线程(BIO Thread)。
- 主线程:负责接收连接、解析请求、读写 Socket、执行命令、定时任务。
- 后台线程:主要用于耗时的文件操作,如
fsync(AOF刷盘)。
3.2 Redis 4.0:Lazy Free 的引入
为了解决删除大 Key(如几百万元素的 Hash)导致主线程阻塞的问题,Redis 4.0 引入了 UNLINK 命令和 FLUSHALL ASYNC。
- 机制:将释放内存的操作通过
bio_job扔给后台线程异步执行,主线程只做逻辑删除。
3.3 Redis 6.0:多线程 IO (Threaded I/O)
为什么引入多线程?
随着网络硬件的发展,Redis 的瓶颈逐渐从 CPU 转移到了 网络带宽 和 网络 I/O 处理(Socket 的读写和协议解析)。
核心设计理念:
Redis 6.0 的多线程仅仅用于 网络数据的读写和协议解析,而 命令的执行(内存操作)依然是单线程的。
- 这样做的好处:
- 无需引入复杂的锁机制,保证了数据操作的原子性。
- 解决了网络 IO 的瓶颈。
- 代码复杂度增加可控。
多线程 IO 流程图
源码关键点 (networking.c):
Redis 在 beforeSleep 中处理多线程读写分配:
handleClientsWithPendingReadsUsingThreads: 将等待读取的客户端分配给 IO 线程。handleClientsWithPendingWritesUsingThreads: 将等待写入的客户端分配给 IO 线程。- Busy Wait: 主线程在分配完任务后,会自旋等待所有 IO 线程完成工作,然后再统一执行命令。
4. 总结:Redis 为什么这么快?
通过对引擎的拆解,我们可以归纳出 Redis 高性能的“三驾马车”:
564

被折叠的 条评论
为什么被折叠?



