第一部分:Redis 8.4 网络模型宏观架构与源码入口
年前这段时间赋闲在家,我系统整理了 10 篇深度解析 Go 语言的硬核文章,随后又精选了 10 篇当下热门 AI 大模型的技术文章(Token、MCP、Agent、Skills、Vibe Coding 等)。今天,我将视角转向 Redis,从源码层面深入剖析它的网络模型实现,若对 Redis 数据结构感兴趣,也可以看看我这个 Redis 专栏,其中详细剖析了常见的功能实现机制。
Redis 8.4 的高性能本质上仍然建立在 I/O 多路复用与非阻塞 I/O 之上的 Reactor 模型:所有命令的解析与执行始终由主线程以单线程方式顺序完成,以保证无锁与强一致性;多线程仅参与命令执行路径之外的网络 I/O 与数据搬运等辅助阶段,用于缓解 I/O 密集场景下的性能瓶颈,而不引入命令级并发。在此基础上,8.x 版本进一步重构了网络层,引入更清晰的 connection 抽象与 privdata 机制,实现事件库与业务逻辑的更彻底解耦。
1.1 背景:从“协议风波”到“创始回归”的 8.x 时代
分析 Redis 8.4 的源码之前,必须理解它所处的特殊历史节点。2024 年初,Redis 官方宣布将其开源协议从 BSD 变更为 RSALv2/SSPLv1,这一举动引发了开源社区的巨大震动,随后诞生了 Valkey、Redict 等重要分支。
就在社区分裂的阴霾下,Redis 之父 Salvatore Sanfilippo(antirez)以技术顾问的身份重新回归社区,并参与到了 Redis 8.0 这一里程碑版本的架构设计中。Redis 8.4 正是这一“文艺复兴”时期的产物。为了应对来自社区分支的竞争,8.4 版本在性能优化和代码模块化上做了极大的改进,例如我们即将在源码中看到的 aeEventLoop 的重构,就是为了让 Redis 核心逻辑更加解耦、更易于测试和扩展。
1.2 核心源码文件地图
在 Redis 8.4 的 src 目录下,网络模型的核心逻辑分布在以下文件:
- ae.c / ae.h:事件驱动框架。封装了 epoll、kqueue 等,提供统一的事件循环接口。
- ae_epoll.c:Linux 环境下具体的 I/O 多路复用实现,直接封装 epoll_wait。
- networking.c:客户端连接管理、读写缓冲区操作、多线程 I/O 的任务分发。
- connection.c / connection.h:连接层抽象。将 TCP、TLS、Unix Socket 统一抽象为 connection 对象。
- resp_parser.c:Redis 协议(RESP2/3)解析器的核心实现。
- server.c:主程序入口,负责调用 initServer 进行网络初始化并启动事件循环。
- anet.c:底层的网络原语封装,如 TCP_NODELAY 设置、Socket 创建等。
1.3 核心数据结构:aeEventLoop 与 client
1.3.1 事件循环结构体 aeEventLoop
在 Redis 8.4 的 ae.h 中,aeEventLoop 是整个网络模型的核心。相比早期版本,它通过 privdata[2] 增强了上下文传递能力。
/* 源码位置: src/ae.h */
typedef struct aeEventLoop {
int maxfd; /* 当前注册的最大文件描述符 */
int setsize; /* 监控的最大 FD 数量限制 */
long long timeEventNextId;
aeFileEvent *events; /* 注册的文件事件(数组大小为 setsize) */
aeFiredEvent *fired; /* 就绪的文件事件 */
aeTimeEvent *timeEventHead;
int stop;
void *apidata; /* 对应具体实现(如 ae_epoll_state) */
aeBeforeSleepProc *beforesleep;
aeBeforeSleepProc *aftersleep;
int flags; /* 状态位 */
void *privdata[2]; /* 8.4 引入:存储上下文,privdata[0] 常指向 server 实例 */
} aeEventLoop;
privdata[2] 的设计意图是减少对全局变量 server 的直接引用。在 initServer 流程中,server 的地址会被存入 privdata[0],使得事件库在回调时能更优雅地获取上下文。
1.3.2 客户端抽象 client
每一个网络连接在 Redis 内部都对应一个 client 结构体,其关键的 I/O 字段如下:
/* 源码位置: src/server.h */
typedef struct client {
connection *conn; /* 指向底层连接对象的指针 */
sds querybuf; /* 输入缓冲区,保存原始请求数据 */
size_t querybuf_peak; /* 峰值内存占用记录 */
int argc; /* 解析出的参数个数 */
robj **argv; /* 解析出的参数数组 */
char buf[PROTO_REPLY_CHUNK_BYTES]; /* 16KB 静态回复缓冲区 */
int bufpos; /* 静态缓冲区当前已用字节数 */
list *reply; /* 动态回复链表,处理大数据量响应 */
/* … 状态位及其他字段 */
} client;
1.3.3 初始化流程:从 main 到 aeMain
Redis 8.4 的网络初始化是一个从内核 Socket 绑定到用户态事件驱动框架挂载的过程。
1.3.3.1 事件引擎的初始化:aeCreateEventLoop
在 server.c 的 initServer 函数中,第一步就是创建 aeEventLoop。
/* 源码位置: server.c -> initServer */
server.el = aeCreateEventLoop(server.maxclients + CONFIG_FDSET_INCR);
if (server.el == NULL) {
serverLog(LL_WARNING, "Failed creating the event loop. Out of memory?");
exit(1);
}
/* 8.4 关键点:将 server 实例指针存入 privdata[0] */
server.el->privdata[0] = &server;
aeCreateEventLoop 内部会调用 aeApiCreate。在 Linux 环境下,这对应 ae_epoll.c,它会执行 epoll_create 并初始化 aeApiState。此时,Redis 已经申请好了存放就绪事件的内存空间(fired 数组)。
1.3.3.2 绑定监听与 anet 抽象层
Redis 需要在指定的端口进行监听。这个逻辑在 listenToPort 中完成,它通过 anet.c 封装的系统调用来创建非阻塞的 Socket。
- 流程:listenToPort -> anetTcpServer -> _anetTcpServer -> anetListen。
- 源码细节:
- socket():创建套接字。
- anetSetReuseAddr():设置 SO_REUSEADDR,防止服务重启时端口被占用。
- anetNonBlock():关键步骤,将 FD 设为非阻塞模式。Redis 的所有网络 IO 必须是非阻塞的。
- bind() & listen():绑定端口并开启监听,设置 backlog(由 server.tcp_backlog 配置)。
1.3.3.3 注册 Accept 回调:建立连接的入口
有了监听 FD(文件描述符)后,Redis 必须告诉事件循环:“如果有新连接进来,请叫我。”
在 8.4 中,这个过程通过 connection 层的 connSetReadHandler 完成,而不再是直接操作 aeCreateFileEvent。
/* 源码位置: server.c -> initServer */
for (j = 0; j < server.ipfd_count; j++) {
/* 封装监听 FD 为 connection 对象,并注册 acceptTcpHandler */
connSetReadHandler(server.ipfd_conn[j], acceptTcpHandler);
}
- acceptTcpHandler:这是处理 TCP 连接建立的入口函数。
- 注册逻辑:connSetReadHandler 内部最终会调用 aeCreateFileEvent,将 server.ipfd 注册到 epoll 实例中,关注 AE_READABLE 事件。
1.3.3.4 开启循环:aeMain 与事件处理逻辑
一切就绪后,main 函数最后一行调用 aeMain(server.el),Redis 正式进入阻塞等待状态。
/* 源码位置: ae.c */
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
/* 进入事件处理主逻辑 */
aeProcessEvents(eventLoop, AE_ALL_EVENTS|
AE_CALL_BEFORE_SLEEP|
AE_CALL_AFTER_SLEEP);
}
}
aeProcessEvents 的核心时序:
1.3.3.5 8.4 特性:事件循环的 Flag 控制
在 8.4 的 aeProcessEvents 中,可以通过 eventLoop->flags 精细化控制事件处理行为。例如 AE_DONT_WAIT 标志位可以让循环立即返回而不阻塞,这在 Redis 内部的一些非阻塞测试脚本和特定的子进程同步逻辑中非常有用。
1.4 Redis 8.4 抽象连接层(Connection Layer)
为了支持多种传输协议(TCP、TLS),Redis 8.4 使用了“虚函数表”的设计思想。所有的 I/O 操作都不再直接调用系统 read/write,而是通过 connection 对象。
/* 源码位置: src/connection.h */
struct ConnectionType {
void (*accept)(connection *conn, ConnectionCallbackFunc accept_handler);
int (*write)(connection *conn, const void *buf, size_t len);
int (*read)(connection *conn, void *buf, size_t len);
void (*close)(connection *conn);
/* … */
};
在 connection.c 中,默认的 CT_Socket 实现了原生的 TCP 操作。当一个连接建立时,其 conn->type 会指向对应的 ConnectionType。这种设计使得 Redis 在处理 TLS 时,上层 networking.c 的逻辑保持不变。
1.5 总结:网络模型的本质
Redis 8.4 网络模型的本质是:利用 aeEventLoop 将阻塞的文件描述符抽象为事件,通过 connection 屏蔽传输层差异,最后在主线程的事件循环中通过回调函数(Callback)完成从“连接接收”到“协议解析”再到“命令执行”的全过程。
尽管有 Threaded I/O,但其核心逻辑依然严格遵守 Reactor 模式,多线程仅作为读写缓冲区的加速器。
为了让你的博文第一部分更具视觉冲击力并清晰展现 Redis 8.4 的启动逻辑,我为你设计了一个 Mermaid 图。这个图结合了函数调用链路与核心数据结构的映射关系。
#mermaid-svg-0yOY2GoFfHCYNNGa{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0yOY2GoFfHCYNNGa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0yOY2GoFfHCYNNGa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0yOY2GoFfHCYNNGa .error-icon{fill:#552222;}#mermaid-svg-0yOY2GoFfHCYNNGa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0yOY2GoFfHCYNNGa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0yOY2GoFfHCYNNGa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0yOY2GoFfHCYNNGa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0yOY2GoFfHCYNNGa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0yOY2GoFfHCYNNGa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0yOY2GoFfHCYNNGa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0yOY2GoFfHCYNNGa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0yOY2GoFfHCYNNGa .marker.cross{stroke:#333333;}#mermaid-svg-0yOY2GoFfHCYNNGa svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0yOY2GoFfHCYNNGa p{margin:0;}#mermaid-svg-0yOY2GoFfHCYNNGa .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-0yOY2GoFfHCYNNGa .cluster-label text{fill:#333;}#mermaid-svg-0yOY2GoFfHCYNNGa .cluster-label span{color:#333;}#mermaid-svg-0yOY2GoFfHCYNNGa .cluster-label span p{background-color:transparent;}#mermaid-svg-0yOY2GoFfHCYNNGa .label text,#mermaid-svg-0yOY2GoFfHCYNNGa span{fill:#333;color:#333;}#mermaid-svg-0yOY2GoFfHCYNNGa .node rect,#mermaid-svg-0yOY2GoFfHCYNNGa .node circle,#mermaid-svg-0yOY2GoFfHCYNNGa .node ellipse,#mermaid-svg-0yOY2GoFfHCYNNGa .node polygon,#mermaid-svg-0yOY2GoFfHCYNNGa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0yOY2GoFfHCYNNGa .rough-node .label text,#mermaid-svg-0yOY2GoFfHCYNNGa .node .label text,#mermaid-svg-0yOY2GoFfHCYNNGa .image-shape .label,#mermaid-svg-0yOY2GoFfHCYNNGa .icon-shape .label{text-anchor:middle;}#mermaid-svg-0yOY2GoFfHCYNNGa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0yOY2GoFfHCYNNGa .rough-node .label,#mermaid-svg-0yOY2GoFfHCYNNGa .node .label,#mermaid-svg-0yOY2GoFfHCYNNGa .image-shape .label,#mermaid-svg-0yOY2GoFfHCYNNGa .icon-shape .label{text-align:center;}#mermaid-svg-0yOY2GoFfHCYNNGa .node.clickable{cursor:pointer;}#mermaid-svg-0yOY2GoFfHCYNNGa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0yOY2GoFfHCYNNGa .arrowheadPath{fill:#333333;}#mermaid-svg-0yOY2GoFfHCYNNGa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0yOY2GoFfHCYNNGa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0yOY2GoFfHCYNNGa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0yOY2GoFfHCYNNGa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0yOY2GoFfHCYNNGa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0yOY2GoFfHCYNNGa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0yOY2GoFfHCYNNGa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0yOY2GoFfHCYNNGa .cluster text{fill:#333;}#mermaid-svg-0yOY2GoFfHCYNNGa .cluster span{color:#333;}#mermaid-svg-0yOY2GoFfHCYNNGa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0yOY2GoFfHCYNNGa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0yOY2GoFfHCYNNGa rect.text{fill:none;stroke-width:0;}#mermaid-svg-0yOY2GoFfHCYNNGa .icon-shape,#mermaid-svg-0yOY2GoFfHCYNNGa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0yOY2GoFfHCYNNGa .icon-shape p,#mermaid-svg-0yOY2GoFfHCYNNGa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0yOY2GoFfHCYNNGa .icon-shape rect,#mermaid-svg-0yOY2GoFfHCYNNGa .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0yOY2GoFfHCYNNGa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0yOY2GoFfHCYNNGa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0yOY2GoFfHCYNNGa :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-0yOY2GoFfHCYNNGa .mainNode>*{fill:#2d3436!important;stroke:#dfe6e9!important;stroke-width:2px!important;color:#fff!important;font-weight:bold!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .mainNode span{fill:#2d3436!important;stroke:#dfe6e9!important;stroke-width:2px!important;color:#fff!important;font-weight:bold!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .mainNode tspan{fill:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .logicNode>*{fill:#0984e3!important;stroke:#74b9ff!important;stroke-width:1px!important;color:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .logicNode span{fill:#0984e3!important;stroke:#74b9ff!important;stroke-width:1px!important;color:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .logicNode tspan{fill:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .structNode>*{fill:#6c5ce7!important;stroke:#a29bfe!important;stroke-width:1px!important;color:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .structNode span{fill:#6c5ce7!important;stroke:#a29bfe!important;stroke-width:1px!important;color:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .structNode tspan{fill:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .storageNode>*{fill:#00b894!important;stroke:#55efc4!important;stroke-width:1px!important;color:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .storageNode span{fill:#00b894!important;stroke:#55efc4!important;stroke-width:1px!important;color:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .storageNode tspan{fill:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .highlightNode>*{fill:#d63031!important;stroke:#ff7675!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .highlightNode span{fill:#d63031!important;stroke:#ff7675!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-0yOY2GoFfHCYNNGa .highlightNode tspan{fill:#fff!important;}
5. 事件循环流转 – aeMain
4. 业务处理层 – networking.c
3. 连接抽象层 – connection.c
2. 事件驱动内核 – ae.c
1. 启动初始化 – server.c
放入任务
锁定执行
回调调用
触发解析
main()
initServer()
listenToPort() (anet.c)
connSetReadHandler(绑定监听事件)
aeEventLoop(事件循环核心)
privdata[2](0:server指针 / 1:ctx)
ae_epoll.c / ae_io_uring.c
ConnectionType(TCP/TLS/Unix 虚函数表)
connection 实例(fd, read/write_handler)
client 结构体(状态机/命令执行)
Pending Queues(read/write 待处理队列)
querybuf (SDS) / Reply List
beforeSleep()(分发多线程IO任务)
aeApiPoll()(epoll_wait)
事件回调处理(遍历 fired 数组)
I/O Threads(并行解析/并行写缓冲)
图表说明(可作为博文注释):
- beforeSleep:这是 Redis 网络模型的“黄金地带”。很多非异步的 IO 优化(如 Threaded IO 的任务分发)都在进入 epoll_wait 之前完成。
- aeApiPoll:根据编译选项,这里会动态指向 ae_epoll.c 或最新的 ae_io_uring.c。
- Handlers:当一个读事件触发时,它会流转到 client 结构体,开始进行 RESP 协议解析。
第二部分:事件驱动引擎 ae 的封装与实现
Redis 的 ae(Async Event)引擎是一个高度精简的 Reactor 框架。在 Redis 8.4 中,它依然保持了不到 1000 行 C 代码的极致简洁,通过对多路复用 API 的静态封装,实现了高性能的事件循环。
2.1 事件状态管理:aeEventLoop 与核心结构
Redis 依然坚持使用简单直接的数组索引来管理文件描述符(FD)。这种 O(1) 的查找方式规避了哈希表或平衡树带来的时间复杂度毛刺。
2.1.1 核心数据结构
/* 源码位置: src/ae.h (Redis 8.4) */
typedef struct aeFileEvent {
int mask; /* AE_READABLE, AE_WRITABLE, AE_BARRIER */
aeFileProc *rfileProc; /* 读回调 */
aeFileProc *wfileProc; /* 写回调 */
void *clientData; /* 指向 connection 或 client 实例 */
} aeFileEvent;
typedef struct aeEventLoop {
int maxfd;
int setsize;
aeFileEvent *events; /* 注册事件数组,下标即 FD */
aeFiredEvent *fired; /* 已就绪事件数组 */
aeTimeEvent *timeEventHead;
void *apidata; /* 指向具体后端(如 aeApiState) */
aeBeforeSleepProc *beforesleep;
aeBeforeSleepProc *aftersleep;
void *privdata; /* 全局上下文指针,指向 RedisServer */
int flags;
} aeEventLoop;
2.2 多路复用层的适配:统一抽象与后端选择
Redis 的 ae 引擎之所以不依赖任何第三方库,是因为它在源码级别实现了一套“静态多态”的适配层。
2.2.1 编译期后端选择机制
Redis 并不在运行时判断环境,而是在编译期通过预处理器决定包含哪个后端。在 ae.c 的末尾,优先级如下:
/* 源码位置: src/ae.c */
#ifdef HAVE_IO_URING
#include "ae_io_uring.c" /* Linux 5.1+ 且开启了 io_uring 支持 */
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c" /* Linux 默认 */
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c" /* macOS/FreeBSD */
#else
#include "ae_select.c" /* 保底方案 */
#endif
#endif
#endif
2.2.2 统一 API 接口契约:解耦的边界
为了屏蔽内核差异,Redis 定义了一套标准的 API 签名。这套契约规定了无论底层是 epoll、kqueue 还是 io_uring,都必须向 ae.c 提供一组功能一致的函数。
主要接口如下:
- aeApiCreate: 分配并初始化后端私有状态(apidata)。
- aeApiAddEvent / aeApiDelEvent: 注册或删除 FD 关注的事件。
- aeApiPoll: 核心阻塞函数,负责收集就绪事件并填充到 eventLoop->fired。
2.2.3 掩码映射:契约背后的“语义翻译”
2.2.2 定义了“做什么”,而 2.2.3 解决了“怎么说”的问题。 这是接口契约能够落地的核心逻辑。
由于不同内核对“可读”或“可写”的描述符完全不同(例如 Linux 用位掩码 EPOLLIN,而 BSD 用常量 EVFILT_READ),每个后端实现在执行 2.2.2 定义的接口时,第一步就是进行 逻辑掩码到内核原生标志的翻译。
2.2.3.1. 映射流程分析
以 aeApiAddEvent 为例,其内部逻辑展示了两者的耦合关系:
/* 这种“契约 + 映射”的模式存在于每个 ae_*.c 中 */
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
// [2.2.3 的核心:掩码翻译]
// 将 ae 层的 AE_READABLE/AE_WRITABLE 转换为内核原生标志
struct epoll_event ee = {0};
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
// [2.2.2 的实现:执行系统调用]
if (epoll_ctl(state->epfd, op, fd, &ee) == –1) return –1;
return 0;
}
2.2.3.2. 对比:不同平台的翻译差异
通过这种映射机制,ae 引擎在保持顶层逻辑简洁的同时,兼容了完全不同的通知模型:
| AE_READABLE | EPOLLIN | EVFILT_READ | POLLIN |
| AE_WRITABLE | EPOLLOUT | EVFILT_WRITE | POLLOUT |
| 映射逻辑 | 位运算 (OR) | 结构体数组转换 | SQE 属性设置 |
2.2.4 为什么这种设计是高效的?
- 零抽象开销:由于映射是在编译期通过 include 确定的,没有虚函数表(vtable)的运行时开销。
- 语义对齐:ae.c 只需要处理 AE_READABLE 这种逻辑状态,而无需关心底层是边缘触发(Edge Triggered)还是水平触发(Level Triggered),所有的适配细节都被封锁在 ae_*.c 的映射层内。
- 防御 FD 复用:在 8.4 源码的 aeApiPoll 映射中,Redis 会根据 fired 数组的反馈动态更新 eventLoop->events,这种双向映射确保了在高并发 FD 频繁关闭和重用时,事件不会被误触发。
2.3 详解 AE_BARRIER:读写顺序的“反转屏障”
这是 Redis 网络模型中最核心的优化逻辑。在标准 Reactor 中,若 FD 同时可读写,通常是“先读后写”。但为了优化响应延迟,Redis 8.4 强化了 AE_BARRIER 逻辑。
2.3.1 核心逻辑:invert 分支控制
在 aeProcessEvents 循环中,通过检查 AE_BARRIER 改变回调顺序:
int invert = fe->mask & AE_BARRIER;
/* 1. 情况 A:正常顺序 (No Barrier) —— 先读 */
if (!invert && (mask & AE_READABLE)) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
fe = &eventLoop->events[fd]; /* 刷新指针,防止读回调内关闭了 FD */
}
/* 2. 写回调执行 */
if (mask & AE_WRITABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
/* 3. 情况 B:屏障反转 (With Barrier) —— 读在写之后 */
if (invert && (mask & AE_READABLE)) {
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
2.3.2 为什么需要反转?
- 降低 Pipeline 延迟:当一个连接既有待发送数据又有待读取请求时,先执行写操作可以将上个请求的回复立即发回,避免其在内存中多待一个循环周期。
- 确保同步:在 beforesleep 阶段处理回复刷新时,如果因为内核缓冲区满而注册了写事件,设置 Barrier 可以确保下次循环“先清空旧回复,再读入新请求”,保证了处理逻辑的严格线性。
2.4 核心处理函数 aeProcessEvents 的深度执行流
这是 Redis 的“心脏”。在 8.4 中,它严密控制着主线程、IO 线程与内核的通信节奏。
2.4.1 aeProcessEvents 核心源码解读
/* 源码位置: src/ae.c (经过精简以突出核心流) */
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
int processed = 0, numevents;
/* 1. 计算阻塞时间:如果设置了定时器,则计算最近一个定时器还剩多久触发 */
struct timespec tv, *tvp = NULL;
if (!(flags & AE_DONT_WAIT))
tvp = aeCalculatePollWait(eventLoop, &tv);
/* 2. BeforeSleep:进入系统调用前的最后机会 */
/* 这里处理:Threaded-IO 任务分发、清除无效连接、AOF 延时写入等 */
if (eventLoop->beforesleep != NULL && (flags & AE_CALL_BEFORE_SLEEP))
eventLoop->beforesleep(eventLoop);
/* 3. 核心阻塞点:调用封装好的多路复用 API (epoll/io_uring) */
numevents = aeApiPoll(eventLoop, tvp);
/* 4. AfterSleep:从内核态唤醒后的即时钩子 */
/* 这里处理:由模块(Modules)触发的阻塞解除、监控统计等 */
if (eventLoop->aftersleep != NULL && (flags & AE_CALL_AFTER_SLEEP))
eventLoop->aftersleep(eventLoop);
/* 5. 遍历已就绪的事件数组 fired[] */
for (int j = 0; j < numevents; j++) {
int fd = eventLoop->fired[j].fd;
aeFileEvent *fe = &eventLoop->events[fd];
int mask = eventLoop->fired[j].mask;
int fired = 0;
/* — 核心逻辑:AE_BARRIER 顺序控制 — */
int invert = fe->mask & AE_BARRIER;
/* 情况 A: 需要反转 (Barrier置位) -> 先写后读 */
if (invert && (mask & AE_READABLE)) {
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
fe = &eventLoop->events[fd]; // 刷新指针,防止回调内关闭了fd
}
}
/* 普通情况:读操作始终优先处理 */
if (!invert && (mask & AE_READABLE)) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
fe = &eventLoop->events[fd];
}
/* 写操作处理 */
if (mask & AE_WRITABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
/* 情况 B: 需要反转 (Barrier置位) -> 此时才执行读 */
if (invert && (mask & AE_READABLE) && !fired) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
processed++;
}
/* 6. 处理时间事件:执行 serverCron 等定时任务 */
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed;
}
2.4.2 深度执行流分析
时间的精确调度 (aeCalculatePollWait): Redis 并非盲目阻塞。它会检查时间事件链表,计算出距离最近一个任务(如 serverCron)还需要多少毫秒,并将该值作为 aeApiPoll 的超时时间。这确保了 Redis 既能高效处理 IO,又不会错过定时任务。
beforesleep 的战略意义: 这是 Redis 性能优化的核心。在 8.4 中,Threaded-IO 的读写任务是在这里派发给 IO 线程的。这意味着在主线程进入 aeApiPoll 阻塞等待新事件时,IO 线程正在并发地处理旧请求的编码或新请求的解析。
AE_BARRIER 的代码体现: 观察源码中的 invert 逻辑。在标准 Reactor 中,读事件总是先于写事件。但 Redis 通过 invert 标志,允许在同一个循环内先执行写回调(回复客户端)再执行读回调(读取新请求)。这种精细控制能显著压低平均响应延迟(RTT)。
防御性编程(FD 刷新): 注意代码中的 fe = &eventLoop->events[fd]。在执行完读回调后,Redis 会重新获取一次 fe 指针。这是因为读回调函数可能会因为逻辑异常(如客户端非法请求)直接关闭并释放当前连接。如果不刷新指针,后续的写操作将会访问野指针,导致 Segmentation Fault。
2.5 Redis 8.4 对 io_uring 的集成分析:ae_io_uring.c
在 Linux 5.1+ 内核中,io_uring 的出现打破了 epoll 长期以来的统治地位。Redis 8.4 通过 ae_io_uring.c 实现了对这一新特性的可选支持,将其作为高性能环境下的首选后端。
2.5.1 什么是 io_uring?—— 革命性的“共享内存”异步 IO
io_uring 是由 Linux 内核大师 Jens Axboe(也是 fio 的作者)在 2019 年引入的一套全新的异步 IO 接口。
在它出现之前,Linux 主要依靠 select/poll/epoll 处理网络 IO,依靠 AIO 处理磁盘 IO。但这些方案在高并发(每秒百万级请求)下都面临着同样的阿喀琉斯之踵:
io_uring 的核心突破在于“内核旁路”设计:它在用户态和内核态之间建立了两块共享内存区域,即两个环形缓冲区(Ring Buffer):
- SQ (Submission Queue):提交队列。用户态把任务(如“监听 FD”)放进去。
- CQ (Completion Queue):完成队列。内核完成任务后把结果放进去。
形象比喻:
- epoll 模式:你每次寄信(读写)都要亲自跑一趟邮局,在柜台排队等办事员处理。
- io_uring 模式:你在家门口装了一个双向快递柜。你想寄信就放进“提交箱”(SQ),邮差取走处理后,把回信放进“收件箱”(CQ)。你只需要定期看一眼快递柜,全程无需跑邮局(减少系统调用)。
2.5.2 SQ 与 CQ 的管理:Redis 的适配逻辑
在 ae_io_uring.c 中,Redis 将这套机制无缝嵌入了 Reactor 模型:
2.5.3 核心实现片段
/* ae_io_uring.c 中的 poll 实现简述 */
static int aeApiPoll(aeEventLoop *eventLoop, struct timespec *tvp) {
aeApiState *state = eventLoop->apidata;
struct io_uring_cqe *cqe;
unsigned head;
/* 1. 批量提交 SQ 中的所有变更,并等待事件(如有必要) */
io_uring_submit_and_wait(&state->ring, wait_nr);
/* 2. 直接遍历 CQ 环形缓冲区,获取就绪 IO 事件 */
io_uring_for_each_cqe(&state->ring, head, cqe) {
struct ae_uring_user_data *data = io_uring_cqe_get_data(cqe);
// 将 cqe 中的结果映射回 eventLoop->fired 数组
...
}
// 3. 更新完成队列头指针,告知内核已处理
io_uring_cq_advance(&state->ring, count);
}
2.5.4 性能收益与 8.4 的深度优化
- Batching(批量化提交):在极高并发连接变动(如大量短连接接入)时,io_uring 相比 epoll 能显著减少 epoll_ctl 产生的 CPU 态切换开销。
- 减少数据拷贝:由于使用共享内存,内核与 Redis 之间不需要反复拷贝事件描述符结构体。
- SQPOLL(内核轮询)支持:如果开启了 IORING_SETUP_SQPOLL 标志,内核会启动一个专门的内核线程来扫描 SQ 队列。这意味着 Redis 甚至不需要发起 enter 系统调用来触发提交,实现了真正意义上的 IO 层内核陷入零开销。
2.5.5 Redis 的集成策略:稳健的“演进”
尽管 io_uring 支持完全异步的 read/write,但在 8.4 版本中,Redis 依然主要利用其 POLL_ADD 模式来替代 epoll。这样做是为了在维持单线程 Reactor 状态机简单性的同时,最大限度榨取 Linux 新内核带来的调度红利。对于万兆网卡和超高 QPS 场景,io_uring 相比 epoll 通常能带来 10%~20% 的性能提升。
2.6 网络优化要点总结
第三部分:连接层的抽象设计(Connection Layer)
3.1 演进背景:从“原始 FD”到“传输层无关”
在 Redis 6.0 之前,Redis 直接操作文件描述符(FD),读写逻辑直接耦合 read() 和 write() 系统调用。但随着 Redis 6.0 原生支持 TLS(加密传输)以及 8.4 对复杂网络环境的适配,这种“硬编码”模式难以为继。
3.1.1 为什么需要 connection 抽象层?
3.2 核心结构:connection 与 ConnectionType 虚函数表
Redis 8.4 使用 C 语言实现了类似面向对象的多态性。
3.2.1 ConnectionType:接口契约
每个协议(TCP、TLS、Unix Socket)都实现了一套函数指针:
/* src/connection.h */
struct ConnectionType {
/* 屏蔽差异的关键:统一的读写接口 */
int (*read)(connection *conn, void *buf, size_t len);
int (*write)(connection *conn, const void *buf, size_t len);
int (*accept)(connection *conn, ConnectionCallbackFunc accept_handler);
/* 握手状态轮询 */
int (*handler)(connection *conn);
...
};
3.2.2 connection:带状态的对象
连接对象不再只是一个整数 FD,它拥有生命周期和状态:
struct connection {
ConnectionType *type; /* 指向 CT_TCP 或 CT_TLS */
ConnectionState state; /* CONN_STATE_ACCEPTING(握手中), CONNECTED, CLOSED */
int fd;
void *private_data; /* 指向关联的 client 实例 */
/* 回调函数 */
ConnectionCallbackFunc conn_handler;
...
};
3.3 异步握手状态机:以 TLS 为例
这是连接层最精妙的设计:在连接完全“就绪”之前,业务层 client 是不可见的。
3.3.1 TLS 握手流转过程
- ae 触发读事件 -> 进入握手处理器。
- 调用 SSL_do_handshake()。
- 如果数据不足(WANT_READ),直接返回 ae 循环,等待下一次触发。
结论:这种设计确保了业务逻辑层永远只能读到“已经解密好”的清洁字节流。
3.4 客户端对象 client 的创建:createClient 源码走读
一旦连接层判定连接就绪(TCP 已连或 TLS 已握手),createClient 才会被调用,将 connection 封装为业务实体。
/* src/networking.c (Redis 8.4) */
client *createClient(connection *conn) {
client *c = zmalloc(sizeof(client));
if (conn) {
connNonBlock(conn); /* 确保非阻塞 */
connEnableTcpNoDelay(conn); /* 禁用 Nagle,优化延迟 */
/* 核心绑定:当连接真正有业务数据时,执行 readQueryFromClient */
connSetReadHandler(conn, readQueryFromClient);
connSetPrivateData(conn, c);
}
c->conn = conn;
c->db = &server.db[0];
...
if (conn) linkClient(c); /* 加入全局客户端链表 */
return c;
}
3.5 非阻塞 IO 与内核参数控制
为了支撑万兆网络下的极致性能,Redis 在连接层对内核参数进行了微操:
3.6 总结
Redis 8.4 的连接层实现了 “解耦”与“性能”的平衡:
- 解耦:底层驱动(ae)、传输协议(connection)、业务逻辑(client)三者互不依赖。
- 性能:通过异步握手状态机,即使是昂贵的 TLS 握手也不会阻塞主线程处理其他已连接客户端的请求。
这种设计使得 Redis 具备了极强的扩展性,开发者甚至可以通过实现新的 ConnectionType 来让 Redis 支持如 QUIC 或 HTTP/3 等新型协议,而无需触动核心业务代码。
#mermaid-svg-ahZLT1ZzBsZYggrG{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ahZLT1ZzBsZYggrG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ahZLT1ZzBsZYggrG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ahZLT1ZzBsZYggrG .error-icon{fill:#552222;}#mermaid-svg-ahZLT1ZzBsZYggrG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ahZLT1ZzBsZYggrG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ahZLT1ZzBsZYggrG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ahZLT1ZzBsZYggrG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ahZLT1ZzBsZYggrG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ahZLT1ZzBsZYggrG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ahZLT1ZzBsZYggrG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ahZLT1ZzBsZYggrG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ahZLT1ZzBsZYggrG .marker.cross{stroke:#333333;}#mermaid-svg-ahZLT1ZzBsZYggrG svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ahZLT1ZzBsZYggrG p{margin:0;}#mermaid-svg-ahZLT1ZzBsZYggrG .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ahZLT1ZzBsZYggrG .cluster-label text{fill:#333;}#mermaid-svg-ahZLT1ZzBsZYggrG .cluster-label span{color:#333;}#mermaid-svg-ahZLT1ZzBsZYggrG .cluster-label span p{background-color:transparent;}#mermaid-svg-ahZLT1ZzBsZYggrG .label text,#mermaid-svg-ahZLT1ZzBsZYggrG span{fill:#333;color:#333;}#mermaid-svg-ahZLT1ZzBsZYggrG .node rect,#mermaid-svg-ahZLT1ZzBsZYggrG .node circle,#mermaid-svg-ahZLT1ZzBsZYggrG .node ellipse,#mermaid-svg-ahZLT1ZzBsZYggrG .node polygon,#mermaid-svg-ahZLT1ZzBsZYggrG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ahZLT1ZzBsZYggrG .rough-node .label text,#mermaid-svg-ahZLT1ZzBsZYggrG .node .label text,#mermaid-svg-ahZLT1ZzBsZYggrG .image-shape .label,#mermaid-svg-ahZLT1ZzBsZYggrG .icon-shape .label{text-anchor:middle;}#mermaid-svg-ahZLT1ZzBsZYggrG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ahZLT1ZzBsZYggrG .rough-node .label,#mermaid-svg-ahZLT1ZzBsZYggrG .node .label,#mermaid-svg-ahZLT1ZzBsZYggrG .image-shape .label,#mermaid-svg-ahZLT1ZzBsZYggrG .icon-shape .label{text-align:center;}#mermaid-svg-ahZLT1ZzBsZYggrG .node.clickable{cursor:pointer;}#mermaid-svg-ahZLT1ZzBsZYggrG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ahZLT1ZzBsZYggrG .arrowheadPath{fill:#333333;}#mermaid-svg-ahZLT1ZzBsZYggrG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ahZLT1ZzBsZYggrG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ahZLT1ZzBsZYggrG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ahZLT1ZzBsZYggrG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ahZLT1ZzBsZYggrG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ahZLT1ZzBsZYggrG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ahZLT1ZzBsZYggrG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ahZLT1ZzBsZYggrG .cluster text{fill:#333;}#mermaid-svg-ahZLT1ZzBsZYggrG .cluster span{color:#333;}#mermaid-svg-ahZLT1ZzBsZYggrG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ahZLT1ZzBsZYggrG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ahZLT1ZzBsZYggrG rect.text{fill:none;stroke-width:0;}#mermaid-svg-ahZLT1ZzBsZYggrG .icon-shape,#mermaid-svg-ahZLT1ZzBsZYggrG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ahZLT1ZzBsZYggrG .icon-shape p,#mermaid-svg-ahZLT1ZzBsZYggrG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ahZLT1ZzBsZYggrG .icon-shape rect,#mermaid-svg-ahZLT1ZzBsZYggrG .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ahZLT1ZzBsZYggrG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ahZLT1ZzBsZYggrG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ahZLT1ZzBsZYggrG :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-ahZLT1ZzBsZYggrG .ae_style>*{fill:#e1f5fe!important;stroke:#01579b!important;stroke-width:2px!important;color:#01579b!important;}#mermaid-svg-ahZLT1ZzBsZYggrG .ae_style span{fill:#e1f5fe!important;stroke:#01579b!important;stroke-width:2px!important;color:#01579b!important;}#mermaid-svg-ahZLT1ZzBsZYggrG .ae_style tspan{fill:#01579b!important;}#mermaid-svg-ahZLT1ZzBsZYggrG .conn_style>*{fill:#f3e5f5!important;stroke:#7b1fa2!important;stroke-width:2px!important;color:#7b1fa2!important;}#mermaid-svg-ahZLT1ZzBsZYggrG .conn_style span{fill:#f3e5f5!important;stroke:#7b1fa2!important;stroke-width:2px!important;color:#7b1fa2!important;}#mermaid-svg-ahZLT1ZzBsZYggrG .conn_style tspan{fill:#7b1fa2!important;}#mermaid-svg-ahZLT1ZzBsZYggrG .biz_style>*{fill:#e8f5e9!important;stroke:#2e7d32!important;stroke-width:2px!important;color:#2e7d32!important;}#mermaid-svg-ahZLT1ZzBsZYggrG .biz_style span{fill:#e8f5e9!important;stroke:#2e7d32!important;stroke-width:2px!important;color:#2e7d32!important;}#mermaid-svg-ahZLT1ZzBsZYggrG .biz_style tspan{fill:#2e7d32!important;}#mermaid-svg-ahZLT1ZzBsZYggrG .highlight>*{fill:#fff9c4!important;stroke:#fbc02d!important;stroke-width:2px!important;color:#92400e!important;}#mermaid-svg-ahZLT1ZzBsZYggrG .highlight span{fill:#fff9c4!important;stroke:#fbc02d!important;stroke-width:2px!important;color:#92400e!important;}#mermaid-svg-ahZLT1ZzBsZYggrG .highlight tspan{fill:#92400e!important;}
💡 Client 业务逻辑层 – Application
📡 Connection 抽象层 – Transport
⚙️ AE 事件驱动引擎 – Reactor
新连接
既有数据
TLS
TCP
未完成
握手成功
触发握手迭代
下一次事件触发
aeApiPoll() 唤醒
事件类型?
acceptTcpHandler
connHandleEvent
connCreateAccept()
传输协议?
🔒 TLS 握手状态机
✅ Connected 状态
注册 tlsHandshakeHandler
退出并交还控制权
createClient()
注册 readQueryFromClient
RESP 协议解析 & 执行
第四部分:命令读取与协议解析全链路分析
4.1 读事件回调:readQueryFromClient 的执行时机
当客户端发送数据(如 SET 命令)到达内核缓冲区,ae 引擎触发读事件,调用 readQueryFromClient。但在 8.4 中,这里存在一个性能分水岭:
4.1.1 串行与并行的抉择
Redis 8.4 默认在主线程同步处理读请求。但如果在 redis.conf 中开启了 Threaded-IO:
io-threads 4
io-threads-do-reads yes
源码逻辑如下:
/* src/networking.c */
void readQueryFromClient(connection *conn) {
client *c = connGetPrivateData(conn);
// 核心逻辑:判断是否推迟读取
if (postponeClientRead(c)) return;
/* 如果不满足多线程条件(如数据量极小),则立即执行同步读取 */
readAndParse(c);
}
- 执行时机:如果满足多线程条件,主线程仅将 c 放入 server.clients_pending_read 队列。真正的 read() 系统调用会推迟到 aeProcessEvents 的 beforesleep 阶段,由 IO 线程并行执行。
4.2 输入缓冲区管理:querybuf 的分配与动态扩容策略
无论是主线程还是 IO 线程,读取到的原始字节 *3\\r\\n$3\\r\\nSET… 都会存入客户端的 c->querybuf 中。
4.2.1 动态伸缩机制
- 初始分配:Redis 倾向于一次读取 PROTO_IOBUF_LEN(16KB)的数据。
- 扩容 (SDS 逻辑):querybuf 是一个 SDS 字符串。通过 sdsMakeRoomFor 保证缓冲区有足够余量。
- 硬限防护:Redis 8.4 严格限制 querybuf 大小。/* 如果客户端持续发送数据且不被解析(如攻击),超过 1GB 将被强制关闭 */
if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
sdsfree(c->querybuf);
c->querybuf = sdsempty();
freeClient(c);
}
4.3 协议解析引擎:resp_parser.c 中的状态机实现
当数据进入缓冲区后,processInputBuffer 函数启动 RESP 解析引擎。
4.3.1 RESP2 与 RESP3 协议的兼容处理
Redis 8.4 会根据首字节自动识别协议类型。
- 以 SET name molaifeng 为例:
- 解析器识别出 *3\\r\\n,判定为 Multibulk(数组)模式,设置 c->multibulklen = 3。
- 分配 c->argv 指针数组。
- 依次读取 $3\\r\\nSET\\r\\n、$4\\r\\nname\\r\\n、$9\\r\\nmolaifeng\\r\\n。
- 状态机转移:解析器记录当前读取的偏移量 c->qb_pos。如果数据不完整(如只收到一半),状态机会保留当前进度,退出解析,等待下次读事件触发。
4.3.2 内联命令(Inline Commands)的特殊解析
如果用户通过 telnet 直接输入 PING\\r\\n(非 * 开头),Redis 会进入 processInlineBuffer。
- 它使用空格分割字符串,将 PING 转换为 c->argv[0]。
- 注意:内联命令主要用于调试,生产环境下高性能客户端均使用标准的 Multibulk 协议。
4.4 命令查找:lookupCommand 与哈希表检索
当 c->argv 被填充完毕(即 argc=3, argv=["SET", "name", "molaifeng"]),解析阶段宣告结束,进入执行准备阶段。
4.4.1 哈希表检索
Redis 在全局 server.commands 字典中查找第一个参数:
struct redisCommand *lookupCommand(sds name) {
return dictFetchValue(server.commands, name);
}
- 对于 SET,它返回一个指向 setCommand 结构的指针。该结构包含了该命令的属性:如 flags(写命令、原子性)、arity(参数个数要求)。
4.4.2 全链路最终步骤:命令分发 (Dispatch)
找到命令后,主线程会进行最后几项关键检查,这也是 8.4 安全性的体现:
≥
3
\\ge 3
≥3。
c->cmd->proc(c); // 此时跳转至 setCommand 函数
setCommand 随后调用底层 dbAdd 函数,将 "name" 和 "molaifeng" 写入全局哈希表。
4.5 案例总结:SET name molaifeng 的全旅程
这种设计确保了 Redis 即使在处理极其复杂的 RESP3 嵌套协议时,依然能保持 O(1) 级别的解析性能。
Dict (Memory)
Main Thread (Redis Core)
IO Thread (8.4)
Client (molaifeng)
Dict (Memory)
Main Thread (Redis Core)
IO Thread (8.4)
Client (molaifeng)
#mermaid-svg-UmlZg1ynmR7xwXJu{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UmlZg1ynmR7xwXJu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UmlZg1ynmR7xwXJu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UmlZg1ynmR7xwXJu .error-icon{fill:#552222;}#mermaid-svg-UmlZg1ynmR7xwXJu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UmlZg1ynmR7xwXJu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UmlZg1ynmR7xwXJu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UmlZg1ynmR7xwXJu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UmlZg1ynmR7xwXJu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UmlZg1ynmR7xwXJu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UmlZg1ynmR7xwXJu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UmlZg1ynmR7xwXJu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UmlZg1ynmR7xwXJu .marker.cross{stroke:#333333;}#mermaid-svg-UmlZg1ynmR7xwXJu svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UmlZg1ynmR7xwXJu p{margin:0;}#mermaid-svg-UmlZg1ynmR7xwXJu .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UmlZg1ynmR7xwXJu text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-UmlZg1ynmR7xwXJu .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-UmlZg1ynmR7xwXJu .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-UmlZg1ynmR7xwXJu .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-UmlZg1ynmR7xwXJu .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-UmlZg1ynmR7xwXJu #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-UmlZg1ynmR7xwXJu .sequenceNumber{fill:white;}#mermaid-svg-UmlZg1ynmR7xwXJu #sequencenumber{fill:#333;}#mermaid-svg-UmlZg1ynmR7xwXJu #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-UmlZg1ynmR7xwXJu .messageText{fill:#333;stroke:none;}#mermaid-svg-UmlZg1ynmR7xwXJu .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UmlZg1ynmR7xwXJu .labelText,#mermaid-svg-UmlZg1ynmR7xwXJu .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-UmlZg1ynmR7xwXJu .loopText,#mermaid-svg-UmlZg1ynmR7xwXJu .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-UmlZg1ynmR7xwXJu .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-UmlZg1ynmR7xwXJu .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-UmlZg1ynmR7xwXJu .noteText,#mermaid-svg-UmlZg1ynmR7xwXJu .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-UmlZg1ynmR7xwXJu .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UmlZg1ynmR7xwXJu .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UmlZg1ynmR7xwXJu .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UmlZg1ynmR7xwXJu .actorPopupMenu{position:absolute;}#mermaid-svg-UmlZg1ynmR7xwXJu .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-UmlZg1ynmR7xwXJu .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UmlZg1ynmR7xwXJu .actor-man circle,#mermaid-svg-UmlZg1ynmR7xwXJu line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-UmlZg1ynmR7xwXJu :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
beforesleep 阶段
下一次 beforesleep
发送 *3\\r\\n$3\\r\\nSET…
readQueryFromClient (加入等待队列)
并行执行 read() 并解析协议
lookupCommand("SET")
ACL & Memory Check
setCommand ->> dbAdd("name", "molaifeng")
addReply(shared.ok)
发送 +OK\\r\\n
第五部分:多线程 IO(Threaded IO)的并发协作机制
在 Redis 8.4 中,网络模型经历了自引入多线程以来最彻底的重构。架构从 Redis 6.0/7.0 的 “主线程 EventLoop + Worker 辅助编解码” 演进为 “One EventLoop Per Thread(每线程一事件循环)”。
如果不理解这个根本性的变化,可以将其比作餐厅改革:
- 旧模式(Redis 6/7):主线程是“包工头”,既要通过唯一的 epoll 盯着所有客人的举手(IO 事件),又要负责炒菜(执行命令)。IO 线程只是临时工,包工头把读到的数据塞给他们解压,解压完还得包工头自己处理。
- 新模式(Redis 8.4):主线程升职为“厨师长”,只负责炒菜(内存操作)。IO 线程变成了拥有独立地盘(EventLoop)的“全职服务员”。客人一进门,就分配给某个服务员,从此以后这个客人的点菜(Read)、倒水(Ping/Pong)、结账(Write)全由服务员独立负责,只有下单那一刻才与厨师长交互。
5.1 IO 线程池的初始化:构建独立 Reactor
初始化不再仅仅是启动线程,而是为每个 Worker 线程构建一个独立的 Reactor 模型。
源码实证 (src/iothread.c – initThreadedIO):
void initThreadedIO(void) {
server.io_threads_active = 1;
// …
for (int i = 1; i < server.io_threads_num; i++) {
IOThread *t = &IOThreads[i];
/* [核心变革] 1. 为每个线程创建独立的 EventLoop */
/* 这意味着 IO 线程拥有了自己的 epoll 实例,可以独立监听 fd */
t->el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
/* 2. 建立通信管道 (Pipe/EventFD) */
/* 也就是“内线电话”,用于主线程唤醒沉睡的 IO 线程 */
t->pending_clients_notifier = createEventNotifier();
aeCreateFileEvent(t->el, getReadEventFd(t->pending_clients_notifier),
AE_READABLE, handleClientsFromMainThread, t);
/* 3. 启动线程,进入独立的事件循环 */
pthread_create(&t->tid, NULL, IOThreadMain, (void*)t);
}
}
解析: 代码中的 aeCreateEventLoop 是最确凿的证据。每个 IO 线程现在都是一个独立的 Reactor,通过 epoll_wait 独立管理其辖区内的连接,彻底打破了主线程的单点瓶颈。
5.2 任务分发:连接卸载(Connection Offloading)
任务分发的本质变成了“连接过户”。当新连接建立时,主线程会将连接的所有权(Ownership)永久性地移交给 IO 线程。
流程逻辑:
源码实证 (src/iothread.c):
/* 主线程执行:甩锅 */
void assignClientToIOThread(client *c) {
// 1. 负载均衡算法选出最空闲线程
int min_id = ...;
// 2. [关键] 从主线程 EventLoop 解绑,停止监听该 fd
connUnbindEventLoop(c->conn);
// 3. 放入队列,准备移交
c->io_flags &= ~(CLIENT_IO_READ_ENABLED | CLIENT_IO_WRITE_ENABLED);
listAddNodeTail(mainThreadPendingClientsToIOThreads[c->tid], c);
}
/* IO 线程执行:接管 */
int processClientsFromMainThread(IOThread *t) {
// … 遍历队列 …
if (!connHasEventLoop(c->conn)) {
// 4. [关键] 绑定到 IO 线程自己的 EventLoop
connRebindEventLoop(c->conn, t->el);
connSetReadHandler(c->conn, readQueryFromClient);
}
}
深度架构分析:Syscall 开销与性能权衡(Trade-off)
你可能会敏锐地发现:connUnbind 和 connRebind 实际上对应着底层的 epoll_ctl(DEL) 和 epoll_ctl(ADD) 系统调用。这是否会导致性能问题?
Syscall 开销确实存在: 在连接移交的过程中,确实多出了两次系统调用和一次跨线程上下文切换。如果是在短连接高并发(High CPS)场景下(例如 PHP 短连接频繁重连),这会导致 System CPU 轻微上升。
为什么还要这么设计?(捡西瓜丢芝麻)
- 计算密集型卸载:Redis 的性能瓶颈通常不在于连接建立,而在于请求处理阶段的协议解析(Protocol Parsing)和内存拷贝。将这部分繁重的 CPU 消耗分摊给 IO 线程,其收益远大于连接建立时的少量 Syscall 损耗。
- 内存安全:epoll_ctl 操作仅涉及内核红黑树节点的指针调整,不会导致内存飙升。内存消耗主要取决于 Client 对象的数量(连接数),这在单线程模型中也是一样的。
最佳实践建议: 为了最大化 Redis 8.4 的性能,强烈建议使用连接池(Connection Pooling)。通过保持长连接,将一次“过户”的开销分摊到后续成千上万次的请求处理中,从而实现接近线性的吞吐量增长。
5.3 协作机制:状态位驱动的读写分离
既然连接在 IO 线程手里,数据也在 IO 线程手里,主线程如何执行命令?Redis 8.4 设计了一套精密的队列交换协议。
阶段一:服务员点菜(IO Thread Read)
IO 线程独立监听 Socket 可读事件,调用 readQueryFromClient 读取并解析协议。此时主线程完全不知情。 一旦解析出完整命令,IO 线程停止处理,申请“上报”。
/* src/networking.c */
if (c->running_tid != IOTHREAD_MAIN_THREAD_ID) {
/* 标记:有命令需要执行 */
c->io_flags |= CLIENT_IO_PENDING_COMMAND;
/* 放入“待处理”队列,并通知主线程 */
enqueuePendingClientsToMainThread(c, 0);
}
阶段二:厨师炒菜(Main Thread Execute)
主线程收到通知,临时接管 Client 对象,执行核心逻辑(操作字典、写 AOF 等)。
/* src/iothread.c */
int processClientsFromIOThread(IOThread *t) {
// …
if (c->io_flags & CLIENT_IO_PENDING_COMMAND) {
/* [核心] 执行命令,结果写入输出缓冲区(内存) */
processPendingCommandAndInputBuffer(c);
}
/* 执行完毕,将 Client 扔回给 IO 线程 */
listLinkNodeHead(mainThreadPendingClientsToIOThreads[c->tid], node);
}
阶段三:服务员上菜(IO Thread Write)
IO 线程重新接管 Client,发现输出缓冲区有数据,调用 writeToClient 将数据推入网卡。
5.4 线程同步原语:EventFD 与自适应锁
在如此高频的交互中,Redis 8.4 放弃了旧版本低效的 Busy Wait(忙轮询),转而采用更现代的同步机制。
1. 唤醒机制:EventFD 取代 While(1)
旧版本主线程在分发任务后会死循环空转等待。8.4 版本中,IO 线程没有任务时会通过 epoll_wait 挂起(Sleep),节省 CPU。主线程通过写 EventFD(或 Pipe)来唤醒它。
/* src/iothread.c */
static inline void sendPendingClientsToMainThreadIfNeeded(...) {
// 仅当接收方处于睡眠或空闲状态时,才触发系统调用唤醒它
if (!running && !pending) {
triggerEventNotifier(mainThreadPendingClientsNotifiers[t->id]);
}
}
2. 锁的粒度:pthread_mutex 的批量操作
虽然引入了锁,但锁的粒度被设计得极粗——只锁“队列交换”的那一瞬间,而不是锁每个请求。
/* 批量接收示例 */
pthread_mutex_lock(&t->pending_clients_mutex);
/* O(1) 操作:瞬间将主线程推过来的整个链表拼接到自己的队列上 */
listJoin(t->pending_clients, mainThreadPendingClientsToIOThreads[i]);
pthread_mutex_unlock(&t->pending_clients_mutex);
这意味着处理 1000 个请求可能只需要争抢一次锁,开销几乎可以忽略不计。
3. 全局暂停:pauseAllIOThreads
当主线程需要进行 Hash Resize 或配置热加载时,需要确保世界静止。Redis 实现了一个基于原子变量的“红绿灯”机制。
- 主线程设置 t->paused = PAUSING。
- IO 线程在下一次循环前检查该变量,主动进入死循环自旋(Spin)或休眠状态,直到主线程解除暂停。
/* IO 线程的自我修养 */
void handlePauseAndResume(IOThread *t) {
int paused;
atomicGetWithSync(t->paused, paused);
if (paused == IO_THREAD_PAUSING) {
// 主动挂起,等待主线程信号
while (paused != IO_THREAD_RESUMING) {
atomicGetWithSync(t->paused, paused);
}
}
}
IO线程(Waiter)
交换队列
主线程(Chef)
客户端
IO线程(Waiter)
交换队列
主线程(Chef)
客户端
#mermaid-svg-IAb2vTYFJ5PbCW1d{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IAb2vTYFJ5PbCW1d .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IAb2vTYFJ5PbCW1d .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IAb2vTYFJ5PbCW1d .error-icon{fill:#552222;}#mermaid-svg-IAb2vTYFJ5PbCW1d .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IAb2vTYFJ5PbCW1d .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IAb2vTYFJ5PbCW1d .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IAb2vTYFJ5PbCW1d .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IAb2vTYFJ5PbCW1d .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IAb2vTYFJ5PbCW1d .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IAb2vTYFJ5PbCW1d .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IAb2vTYFJ5PbCW1d .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IAb2vTYFJ5PbCW1d .marker.cross{stroke:#333333;}#mermaid-svg-IAb2vTYFJ5PbCW1d svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IAb2vTYFJ5PbCW1d p{margin:0;}#mermaid-svg-IAb2vTYFJ5PbCW1d .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IAb2vTYFJ5PbCW1d text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-IAb2vTYFJ5PbCW1d .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IAb2vTYFJ5PbCW1d .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-IAb2vTYFJ5PbCW1d .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-IAb2vTYFJ5PbCW1d .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-IAb2vTYFJ5PbCW1d #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-IAb2vTYFJ5PbCW1d .sequenceNumber{fill:white;}#mermaid-svg-IAb2vTYFJ5PbCW1d #sequencenumber{fill:#333;}#mermaid-svg-IAb2vTYFJ5PbCW1d #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-IAb2vTYFJ5PbCW1d .messageText{fill:#333;stroke:none;}#mermaid-svg-IAb2vTYFJ5PbCW1d .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IAb2vTYFJ5PbCW1d .labelText,#mermaid-svg-IAb2vTYFJ5PbCW1d .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-IAb2vTYFJ5PbCW1d .loopText,#mermaid-svg-IAb2vTYFJ5PbCW1d .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-IAb2vTYFJ5PbCW1d .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IAb2vTYFJ5PbCW1d .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-IAb2vTYFJ5PbCW1d .noteText,#mermaid-svg-IAb2vTYFJ5PbCW1d .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-IAb2vTYFJ5PbCW1d .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IAb2vTYFJ5PbCW1d .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IAb2vTYFJ5PbCW1d .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IAb2vTYFJ5PbCW1d .actorPopupMenu{position:absolute;}#mermaid-svg-IAb2vTYFJ5PbCW1d .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-IAb2vTYFJ5PbCW1d .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IAb2vTYFJ5PbCW1d .actor-man circle,#mermaid-svg-IAb2vTYFJ5PbCW1d line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-IAb2vTYFJ5PbCW1d :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
阶段一:连接卸载 (Connection Offloading)
阶段二:命令读取与解析
阶段三:主线程执行
持有 Client 完全控制权
阶段四:响应回写
1. 发起 TCP 连接
1
accept() 获取 fd
2
epoll_ctl(DEL) 解绑
3
2. Client 指针入队
4
3. 写 EventFD 唤醒 IO 线程
5
取出 Client
6
epoll_ctl(ADD) 绑定IO线程
7
4. 发送命令 (SET k v)
8
readQueryFromClient
9
协议解析
10
5. 标记 Pending Command 入队
11
6. 写 EventFD 唤醒主线程
12
取出 Client
13
processPendingCommand
14
操作 DB / 写 AOF
15
结果写入内存缓冲区
16
7. Client 回收并入队
17
8. 写 EventFD 唤醒 IO 线程
18
取出 Client
19
writeToClient
20
9. 返回结果 OK
21
第六部分:命令执行与回复缓冲区(Output Buffer)管理
当 IO 线程将 Socket 中的数据读取并解析完毕后,client 对象被移交给主线程。此时,Redis 进入了计算密集型阶段。命令的执行结果(Reply)并不会立即触发系统调用写入 Socket,而是先暂存在用户态的输出缓冲区(Output Buffer)中,等待统一调度。
6.1 命令执行的核心入口:call() 函数分析
在 src/server.c 中,call() 函数是命令执行的“总导演”。它不仅负责调用具体的命令实现,还处理了 Slowlog 记录、AOF 传播、监控(Monitor)推送等副作用。
核心流程:
c->cmd->proc(c);
6.2 回复数据的写入链路:addReply 系列函数
在 src/networking.c 中,addReply 及其变体(addReplySds, addReplyBulk 等)负责将数据写入 client 结构体内的缓冲区。
关键源码逻辑 (src/networking.c):
准备阶段 (_prepareClientToWrite): 每次写入前,Redis 都会检查 Client 状态。
- 如果 Client 是 Lua 伪客户端或 Module 伪客户端,直接返回。
- 核心动作:如果当前 Client 还没有被标记为“待写”,调用 putClientInPendingWriteQueue(c)。
- 入队不写:该函数仅将 Client 挂入全局链表 server.clients_pending_write,并标记 CLIENT_PENDING_WRITE。此时绝不会触发 write 系统调用。
数据路由 (_addReplyToBufferOrList): 数据写入哪里?Redis 采用了双层缓冲策略,该函数负责路由决策。
6.3 静态缓冲区 buf 与动态链表 reply 的切换逻辑
为了在“小数据高性能”和“大数据大容量”之间取得平衡,Redis 8.4 设计了两级缓冲机制。
第一级:静态固定缓冲区 (c->buf)
- 定义:client 结构体中内嵌的一个 16KB 字节数组(PROTO_REPLY_CHUNK_BYTES)。
- 优势:零分配(Zero-Allocation)。它随 client 结构体一同分配,不需要额外的 malloc,且 CPU 缓存亲和性极高。
- 策略:只要链表为空且 buf 还有空间,优先写入这里。
第二级:动态响应链表 (c->reply)
- 定义:一个双向链表,节点承载具体的响应数据块。
- 优势:容量无上限(受 maxmemory 限制)。
- 策略:
- 当 c->buf 满溢时,启用链表。
- 为保持 FIFO 顺序,一旦使用了链表,后续所有数据强制追加到链表尾部。
- 内存优化:trimReplyUnusedTailSpace 函数会监控链表尾部节点的利用率。如果尾节点分配了很大空间但只用了很少,Redis 会通过 realloc 缩容,减少内存碎片。
6.4 延迟写回与多线程分发:handleClientsWithPendingWrites 的路由机制
这是 Redis 8.4 网络模型中最关键的分发器(Router)。
在 beforeSleep 阶段,主线程需要处理 server.clients_pending_write 链表中的所有客户端。此时,回复数据已经躺在内存缓冲区里了,接下来的问题是:谁负责把它们发给网卡?
源码深度解析 (src/networking.c):
int handleClientsWithPendingWrites(void) {
listIter li;
listNode *ln;
// … 遍历待写链表 …
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_WRITE; // 清除标记
/* [核心路由] 判断是否开启了多线程 IO */
if (server.io_threads_num > 1 &&
!(c->flags & CLIENT_CLOSE_AFTER_REPLY) &&
!isClientMustHandledByMainThread(c))
{
/* 路径 A:移交给 IO 线程 (Offloading) */
/* 主线程仅仅是将 Client 指针“过户”给 IO 线程,不执行 syscall */
assignClientToIOThread(c);
continue;
}
/* 路径 B:主线程兜底 (Fallback) */
/* 仅在多线程未开启,或 Client 状态特殊(如需立即关闭)时执行 */
if (writeToClient(c,0) == C_ERR) continue;
/* 如果一次没写完,才注册 epoll 写事件 */
if (clientHasPendingReplies(c)) {
installClientWriteHandler(c);
}
}
return processed;
}
路径 A:IO 线程消费(高性能主路径)
这是 Redis 8.4 吞吐量飙升的秘诀。
路径 B:主线程兜底
当 server.io_threads_num == 1 或遇到特殊 Client(如 Debug 客户端、立即关闭的连接)时,主线程会亲自调用 writeToClient。
总结:生产者-消费者模型
Redis 8.4 的输出管理完美诠释了无锁(或低锁)协作:
- 主线程(生产者):负责计算,产出数据到 c->buf/reply。操作纯内存,极快。
- IO 线程(消费者):负责 IO,将 c->buf/reply 的数据冲刷到 Socket。操作 fd,承担系统调用开销。
#mermaid-svg-4fXpT3qUQUueAsWZ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4fXpT3qUQUueAsWZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4fXpT3qUQUueAsWZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4fXpT3qUQUueAsWZ .error-icon{fill:#552222;}#mermaid-svg-4fXpT3qUQUueAsWZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4fXpT3qUQUueAsWZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4fXpT3qUQUueAsWZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4fXpT3qUQUueAsWZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4fXpT3qUQUueAsWZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4fXpT3qUQUueAsWZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4fXpT3qUQUueAsWZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4fXpT3qUQUueAsWZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4fXpT3qUQUueAsWZ .marker.cross{stroke:#333333;}#mermaid-svg-4fXpT3qUQUueAsWZ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4fXpT3qUQUueAsWZ p{margin:0;}#mermaid-svg-4fXpT3qUQUueAsWZ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-4fXpT3qUQUueAsWZ .cluster-label text{fill:#333;}#mermaid-svg-4fXpT3qUQUueAsWZ .cluster-label span{color:#333;}#mermaid-svg-4fXpT3qUQUueAsWZ .cluster-label span p{background-color:transparent;}#mermaid-svg-4fXpT3qUQUueAsWZ .label text,#mermaid-svg-4fXpT3qUQUueAsWZ span{fill:#333;color:#333;}#mermaid-svg-4fXpT3qUQUueAsWZ .node rect,#mermaid-svg-4fXpT3qUQUueAsWZ .node circle,#mermaid-svg-4fXpT3qUQUueAsWZ .node ellipse,#mermaid-svg-4fXpT3qUQUueAsWZ .node polygon,#mermaid-svg-4fXpT3qUQUueAsWZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4fXpT3qUQUueAsWZ .rough-node .label text,#mermaid-svg-4fXpT3qUQUueAsWZ .node .label text,#mermaid-svg-4fXpT3qUQUueAsWZ .image-shape .label,#mermaid-svg-4fXpT3qUQUueAsWZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-4fXpT3qUQUueAsWZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4fXpT3qUQUueAsWZ .rough-node .label,#mermaid-svg-4fXpT3qUQUueAsWZ .node .label,#mermaid-svg-4fXpT3qUQUueAsWZ .image-shape .label,#mermaid-svg-4fXpT3qUQUueAsWZ .icon-shape .label{text-align:center;}#mermaid-svg-4fXpT3qUQUueAsWZ .node.clickable{cursor:pointer;}#mermaid-svg-4fXpT3qUQUueAsWZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4fXpT3qUQUueAsWZ .arrowheadPath{fill:#333333;}#mermaid-svg-4fXpT3qUQUueAsWZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4fXpT3qUQUueAsWZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4fXpT3qUQUueAsWZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4fXpT3qUQUueAsWZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4fXpT3qUQUueAsWZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4fXpT3qUQUueAsWZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4fXpT3qUQUueAsWZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4fXpT3qUQUueAsWZ .cluster text{fill:#333;}#mermaid-svg-4fXpT3qUQUueAsWZ .cluster span{color:#333;}#mermaid-svg-4fXpT3qUQUueAsWZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4fXpT3qUQUueAsWZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4fXpT3qUQUueAsWZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-4fXpT3qUQUueAsWZ .icon-shape,#mermaid-svg-4fXpT3qUQUueAsWZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4fXpT3qUQUueAsWZ .icon-shape p,#mermaid-svg-4fXpT3qUQUueAsWZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4fXpT3qUQUueAsWZ .icon-shape rect,#mermaid-svg-4fXpT3qUQUueAsWZ .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4fXpT3qUQUueAsWZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4fXpT3qUQUueAsWZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4fXpT3qUQUueAsWZ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-4fXpT3qUQUueAsWZ .mainThread>*{fill:#e1f5fe!important;stroke:#01579b!important;stroke-width:2px!important;}#mermaid-svg-4fXpT3qUQUueAsWZ .mainThread span{fill:#e1f5fe!important;stroke:#01579b!important;stroke-width:2px!important;}#mermaid-svg-4fXpT3qUQUueAsWZ .ioThread>*{fill:#fff3e0!important;stroke:#e65100!important;stroke-width:2px!important;}#mermaid-svg-4fXpT3qUQUueAsWZ .ioThread span{fill:#fff3e0!important;stroke:#e65100!important;stroke-width:2px!important;}#mermaid-svg-4fXpT3qUQUueAsWZ .buffer>*{fill:#e8f5e9!important;stroke:#2e7d32!important;stroke-width:2px!important;stroke-dasharray:5 5!important;}#mermaid-svg-4fXpT3qUQUueAsWZ .buffer span{fill:#e8f5e9!important;stroke:#2e7d32!important;stroke-width:2px!important;stroke-dasharray:5 5!important;}#mermaid-svg-4fXpT3qUQUueAsWZ .decision>*{fill:#fff9c4!important;stroke:#fbc02d!important;stroke-width:2px!important;}#mermaid-svg-4fXpT3qUQUueAsWZ .decision span{fill:#fff9c4!important;stroke:#fbc02d!important;stroke-width:2px!important;}
路径 B: 兜底 (Main Thread)
路径 A: 消费者 (IO Thread)
阶段二:路由分发 (Router)
阶段一:生产者 (Main Thread)
链表为空 & buf有空间
buf已满 或 链表非空
进入 beforeSleep
Yes (高性能路径)
No (兜底路径)
Yes
call 命令执行入口
c->cmd->proc 执行业务逻辑
调用 addReply
缓冲区策略判定
写入 c->buf (16KB 静态数组)
追加到 c->reply (动态双向链表)
标记 PENDING_WRITE加入 clients_pending_write
handleClientsWithPendingWrites
路由判断:1. io_threads > 1 ?2. 适合卸载 ?
assignClientToIOThread (连接过户)
唤醒 IO 线程
IO线程处理
Syscall: writev (并发写 Socket)
主线程: writeToClient
Syscall: writev (同步写 Socket)
没写完?
注册 AE_WRITABLE 事件
这是基于 Redis 8.4 源码(src/networking.c)重构后的第七部分。
在上一部分,我们解决了“由谁来写(主线程 vs IO 线程)”的路由问题。这一部分我们将深入到底层,探究“数据是如何被写入 Socket 的”,以及“当数据写不完或积压太多时,Redis 如何自我保护”。
这里展示了 Redis 在系统调用层面的极致优化(Scatter-Gather IO)和在内存管理层面的防御机制。
第七部分:底层 IO 实现与缓冲区溢出保护
7.1 系统调用优化:_writevToClient 的聚集写(Scatter-Gather I/O)实现
Redis 的输出缓冲区由两部分组成:一段连续的静态内存(c->buf)和一个非连续的链表(c->reply)。如果使用普通的 write 系统调用,Redis 需要先发送静态缓冲,再遍历链表逐个发送节点,这将导致多次用户态/内核态切换。
Redis 8.4 使用 writev 系统调用来实现 聚集写(Scatter-Gather I/O),将多个不连续的内存块一次性交给内核发送。
核心源码分析 (src/networking.c – _writevToClient):
static int _writevToClient(client *c, ssize_t *nwritten) {
/* 1. 准备 IO 向量数组 */
/* IOV_MAX 通常是 1024,限制单次系统调用涉及的内存块数量 */
struct iovec iov[IOV_MAX];
int iovcnt = 0;
size_t iov_bytes_len = 0;
/* 2. 装载静态缓冲区 (Static Buffer) */
/* 如果 c->buf 有数据,它总是作为第一个块 */
if (c->bufpos > 0) {
iov[iovcnt].iov_base = c->buf + c->sentlen; // 加上已发送偏移量
iov[iovcnt].iov_len = c->bufpos – c->sentlen;
iov_bytes_len += iov[iovcnt++].iov_len;
}
/* 3. 装载动态链表 (Dynamic Reply List) */
listIter iter;
listNode *next;
clientReplyBlock *o;
listRewind(c->reply, &iter);
/* 遍历链表,填充 iov 数组,直到填满 IOV_MAX 或达到单次写入上限 */
while ((next = listNext(&iter)) && iovcnt < IOV_MAX && iov_bytes_len < NET_MAX_WRITES_PER_EVENT) {
o = listNodeValue(next);
if (o->used == 0) { /* 跳过空节点并清理 */
listDelNode(c->reply, next);
continue;
}
iov[iovcnt].iov_base = o->buf; // 指向链表节点的缓冲区
iov[iovcnt].iov_len = o->used;
iov_bytes_len += iov[iovcnt++].iov_len;
}
/* 4. 执行系统调用 */
if (iovcnt == 0) return C_OK;
*nwritten = connWritev(c->conn, iov, iovcnt); // 底层调用 writev
/* … 后续处理 … */
}
技术亮点:
- 减少 Syscall:将最多 IOV_MAX(通常 1024)个内存块合并为一次系统调用。对于 list range 或大对象获取这类产生大量碎片的命令,吞吐量提升显著。
- 内存零拷贝:writev 直接读取各个内存块,无需先将数据拷贝到一个大的连续 buffer 中再发送。
7.2 兜底机制:写事件回调 sendReplyToClient
在绝大多数情况下,Redis 希望在 beforeSleep 阶段通过直接调用 writeToClient(Fast Path)把数据发完,从而避免注册文件事件的开销。
但如果客户端接收慢,或者数据量过大导致内核 Socket 发送缓冲区满(EAGAIN),数据就会残留。此时 Redis 必须退回到标准的事件驱动模式。
注册逻辑 (src/networking.c – handleClientsWithPendingWrites):
/* 如果尝试直接写之后还有剩余数据 */
if (clientHasPendingReplies(c)) {
/* 注册 AE_WRITABLE 事件,绑定回调函数 sendReplyToClient */
installClientWriteHandler(c);
}
回调逻辑 (src/networking.c – sendReplyToClient): 当 EventLoop 检测到 Socket 可写时,触发此回调:
void sendReplyToClient(connection *conn) {
client *c = connGetPrivateData(conn);
/* 第二个参数 handler_installed = 1 */
writeToClient(c, 1);
}
状态机闭环: writeToClient 内部会再次尝试发送。如果这次发完了,它会主动调用 connSetWriteHandler(c->conn, NULL) 移除写事件监听。这种“按需注册,用完即弃”的策略是 Redis 保持 EventLoop 高效的关键。
7.3 安全防线:输出缓冲区限制(Output Buffer Limits)
如果客户端(消费者)处理速度远慢于 Redis(生产者),或者某个客户端执行了 MONITOR 命令,输出缓冲区(特别是 c->reply 链表)会无限膨胀,最终导致 Redis OOM(内存溢出)。
Redis 8.4 通过 checkClientOutputBufferLimits 实施严格的自我保护。
检测逻辑 (src/networking.c):
int checkClientOutputBufferLimits(client *c) {
/* 计算当前缓冲区总大小 (buf + reply list) */
unsigned long used_mem = getClientOutputBufferMemoryUsage(c);
/* 获取配置限制 (client-output-buffer-limit) */
// class 分为 NORMAL, SLAVE, PUBSUB
int class = getClientType(c);
size_t hard_limit = server.client_obuf_limits[class].hard_limit_bytes;
size_t soft_limit = server.client_obuf_limits[class].soft_limit_bytes;
/* 1. 硬限制检查 (Hard Limit) */
if (hard_limit && used_mem >= hard_limit)
return 1; // 立即断开
/* 2. 软限制检查 (Soft Limit) */
if (soft_limit && used_mem >= soft_limit) {
if (c->obuf_soft_limit_reached_time == 0) {
/* 第一次触达,记录时间 */
c->obuf_soft_limit_reached_time = server.unixtime;
return 0; // 暂时放过
} else {
/* 检查持续时间是否超过阈值 */
time_t elapsed = server.unixtime – c->obuf_soft_limit_reached_time;
if (elapsed > server.client_obuf_limits[class].soft_limit_seconds)
return 1; // 持续超标,断开
}
} else {
c->obuf_soft_limit_reached_time = 0; // 恢复正常,重置计时器
}
return 0;
}
保护机制:一旦该函数返回 1,Redis 会立即调用 freeClientAsync(c) 强制断开连接,并在日志中打印 Client closed for overcoming of output buffer limits,防止单点故障拖垮整个实例。
7.4 内存回收策略:数据游标的推进
当 writev 成功返回 nwritten 字节后,Redis 需要精确地清理已发送的数据,释放内存。
清理流程 (src/networking.c – _writevToClient 后半部分):
消耗静态缓冲区: 首先扣减 c->buf。如果 nwritten 大于 c->bufpos,说明静态区发完了,重置 c->bufpos = 0,剩余的 nwritten 继续用于扣减链表。
消耗动态链表: 遍历 c->reply 链表,逐个节点扣减。
listRewind(c->reply, &iter);
while (remaining > 0) {
next = listNext(&iter);
o = listNodeValue(next);
/* 如果当前节点被完全发送 */
if (remaining >= o->used) {
remaining -= o->used;
listDelNode(c->reply, next); // 关键:释放节点内存 (zfree)
} else {
/* 当前节点只发了一部分,修改偏移量,下次继续发 */
/* 注意:这里并没有 memmove,而是通过偏移量逻辑处理,避免内存拷贝 */
c->sentlen += remaining;
break;
}
}
这种“边发边删”的机制确保了 Redis 在处理大数据传输时,内存占用能够随着网络发送即时下降,而不是等到所有数据发完才统一释放。
第八部分:Redis 8.4 针对高并发 IO 的底层微调
在高并发 IO 场景下,瓶颈往往从网络带宽转移到了 CPU 缓存命中率(Cache Hit Rate) 和 内存分配器(Allocator) 的开销上。Redis 8.4 在这方面进行了极度克制的底层优化。
8.1 缓冲区分配器的改进:对内存对齐与 Cache Line 的利用
Redis 在创建客户端时,需要频繁申请输入输出缓冲区。Redis 8.4 通过巧妙利用内存分配器的特性,榨干了每一个字节。
8.1.1. zmalloc_usable:榨干碎片空间
在 createClient 中,Redis 申请静态输出缓冲区时,并不仅仅是申请固定的 16KB:
/* src/networking.c – createClient */
c->buf = zmalloc_usable(PROTO_REPLY_CHUNK_BYTES, &c->buf_usable_size);
- 原理:虽然宏 PROTO_REPLY_CHUNK_BYTES 定义为 16KB,但底层的分配器(jemalloc/tcmalloc)为了内存对齐,通常会分配比请求稍大一点的内存块(例如请求 16384 字节,实际分配 16400 字节)。
- 优化:zmalloc_usable 会返回实际分配的大小,并将其存入 c->buf_usable_size。Redis 据此调整缓冲区边界。
- 收益:在高并发下,这意味着每数千个连接就能“白嫖”出相当可观的缓存空间,且减少了越界风险,完全消除了内部碎片浪费。
8.1.2. 非贪婪式扩容 (sdsMakeRoomForNonGreedy)
在 readQueryFromClient 中,IO 线程读取数据到输入缓冲区 querybuf 时,使用了特殊的扩容策略:
/* src/networking.c – readQueryFromClient */
if (...) {
/* 使用非贪婪模式扩容,避免分配过多内存导致频繁的 Cache Miss */
c->querybuf = sdsMakeRoomForNonGreedy(c->querybuf, readlen);
} else {
/* 普通模式:指数级扩容(2倍) */
c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
}
- 场景:对于大数据包(Big Arg),Redis 8.4 放弃了传统的指数级(x2)预分配,改用“够用就好”的策略。
- 收益:这避免了处理大请求时 querybuf 瞬间膨胀导致大量物理内存页(Page)被触碰,从而降低了 TLB(Translation Lookaside Buffer)Miss 的概率。
8.2 client 结构体字段重排:减少缓存行失效(Cache Miss)
虽然具体的结构体定义在 server.h,但在 Redis 8.4 的代码逻辑中,可以明显看到对 热点字段(Hot Fields)的访问聚集性优化。
CPU Cache Line 亲和性设计
CPU 读取内存不是一个字节一个字节读,而是一次读取一个 Cache Line(通常 64 字节)。
在 readQueryFromClient 和 writeToClient 这两个最高频的函数中,代码对 client 结构体的访问高度集中在以下字段:
- c->conn (连接指针)
- c->bufpos (静态缓冲偏移)
- c->buf (静态缓冲指针)
- c->querybuf (输入缓冲指针)
- c->flags (状态标记)
代码逻辑体现: Redis 开发者在设计时,有意将这些字段在内存布局上紧密排列。当 CPU 读取 c->conn 准备进行系统调用时,c->buf 和 c->flags 很可能已经被自动加载到了 L1 Cache 中。
- 反例:如果 c->name(很少访问)插在 c->conn 和 c->buf 中间,就会导致 CPU 加载无用数据,浪费宝贵的 Cache 空间。
8.3 异常连接的快速收割逻辑:freeClient 与销毁流程
在高并发场景下,客户端的连接和断开(Churn Rate)非常频繁。如果销毁流程太慢,会导致 fd 耗尽或内存泄漏。Redis 8.4 引入了 异步惰性释放(Async Lazy Free)机制。
8.3.1. freeClientAsync:安全地“判死刑”
在多线程环境下,直接释放一个可能正被 IO 线程操作的 Client 是极度危险的(会导致 Segfault)。
源码分析 (src/networking.c – freeClientAsync):
void freeClientAsync(client *c) {
/* 1. 线程安全检查 */
if (c->running_tid != IOTHREAD_MAIN_THREAD_ID) {
/* 如果 Client 正被 IO 线程持有,不能直接删! */
/* 标记为由 IO 线程关闭,并推回主线程队列 */
c->io_flags |= CLIENT_IO_CLOSE_ASAP;
enqueuePendingClientsToMainThread(c, 1);
return;
}
/* 2. 标记为尽快关闭 */
if (c->flags & CLIENT_CLOSE_ASAP) return;
c->flags |= CLIENT_CLOSE_ASAP;
/* 3. 加入“死亡名单” */
listAddNodeTail(server.clients_to_close, c);
}
8.3.2. 批量收割 (freeClientsInAsyncFreeQueue)
主线程不会在检测到错误时立即调用 free(系统调用开销大),而是将它们挂在 clients_to_close 链表上。 在 beforeSleep 阶段,主线程会集中遍历这个链表,批量销毁:
- 优势:利用 CPU 的 Pipeline,批量处理内存释放,比“零敲碎打”的释放效率更高。
8.3.3. freeClient 的资源解绑
当真正执行销毁时,freeClient 执行了严格的资源解绑顺序,防止悬挂指针:
这种“标记-异步回收-批量释放”的策略,确保了 Redis 在面对每秒数万次连接断开冲击时,主线程依然能保持平滑的响应抖动。
第九部分:总结:Redis 网络模型的演进哲学
Redis 8.4 的网络模型并非推倒重来,而是在保持其核心设计哲学(简单、原子性)的前提下,对性能瓶颈进行的“手术刀式”精准切割。
9.1 为什么坚持单线程执行命令:原子性与复杂度的平衡
在 Redis 8.4 引入了如此复杂的 IO 多线程机制(Threaded IO)后,依然坚守 “命令执行单线程” 的底线。这并非技术上的妥协,而是基于深思熟虑的架构权衡。
9.11. 复杂数据结构的并发噩梦
Redis 不同于 Memcached(简单的 KV),它拥有 Set、ZSet、Hash、Stream 等极其复杂的数据结构。 如果让多线程并发执行命令:
- 锁粒度难题:为了保证 ZADD 或 LPUSH 的线程安全,必须引入行级锁(Key-level Locking)甚至更细粒度的锁。这会带来巨大的锁竞争(Lock Contention)和上下文切换开销,极大地抵消多线程带来的收益。
- 代码复杂度爆炸:Redis 源码目前依然保持着惊人的可读性。一旦引入细粒度锁,死锁检测和并发调试将成为开发者的噩梦。
9.12. 生态系统的基石:原子性
Redis 的生态系统(Lua 脚本、事务 MULTI/EXEC、模块系统)完全建立在“单线程原子执行”的假设之上。
- 现状:开发者默认 EVAL "redis.call('GET', k); redis.call('SET', k, v)" 中间不会插入其他命令。
- 代价:如果改为多线程执行,这种原子性保证将瞬间瓦解,整个 Redis 生态需要重写。
9.13. 瓶颈转移的胜利
Redis 6.0 之前的瓶颈在于网络 IO(读写 Socket 消耗大量 CPU)。Redis 8.4 通过将 read/write 和 parse 剥离给 IO 线程,成功将主线程从“搬运工”解放为纯粹的“计算者”。 结论:在 99% 的场景下,纯内存操作的 CPU 消耗远小于网络 IO。因此,IO 多线程 + 执行单线程 是当前硬件条件下的最优解。
9.2 Redis 8.4 在 Linux 现代内核特性下的性能天花板
通过源码 src/iothread.c,我们可以看到 Redis 8.4 极度压榨了现代 Linux 内核的特性,几乎触及了用户态网络程序的性能天花板。
9.2.1. 自适应互斥锁 (PTHREAD_MUTEX_ADAPTIVE_NP)
在 initThreadedIO 中,Redis 并没有使用默认的互斥锁:
/* src/iothread.c */
pthread_mutexattr_settype(attr, PTHREAD_MUTEX_ADAPTIVE_NP);
这是 Linux 特有的扩展(NP = Non-Portable)。
- 机制:当线程获取锁失败时,它不会立即调用 futex 系统调用进入睡眠(这非常慢),而是先在用户态自旋(Spinning)一段时间。
- 场景:Redis 的队列交换操作极快(仅仅是链表指针移动),锁持有时间极短。自旋锁避免了昂贵的线程挂起/唤醒开销,完美契合 Redis 的高频交互场景。
9.2.2. CPU 亲和性 (sched_setaffinity)
Redis 通过 redisSetCpuAffinity 强制将 IO 线程绑定到特定的 CPU 核心。
- 收益:这确保了每个 IO 线程独占 L1/L2 缓存,极大减少了 Cache Miss,使得处理延迟更加确定(Deterministic)。
9.2.3. 聚集写 (writev) 与 零拷贝
通过 struct iovec 将静态缓冲区和动态链表聚合发送,Redis 减少了系统调用次数,并利用内核能力避免了用户态的多次数据搬运。
9.3 源码阅读建议:如何跟踪一次完整的交互流程(GDB 实战技巧)
这里建议移步我 21年写的博客《Linux 中 gdb 调试 Redis》,这里就不在赘述了。
第十部分:延伸思考:Redis 网络模型 vs Go Netpoller
前段时间写了《Go 语言如何实现高性能网络 I/O:Netpoller 模型揭秘》,感觉可以和 Reids 的网络模型比比。毕竟在现代高并发架构中,Go (Netpoller + GMP) 和 Redis (Threaded IO) 代表了两种解决 C10K/C10M 问题的巅峰思路。将两者进行对比,能让我们更深刻地理解 Redis 8.4 为什么要坚持“怪异”的单线程执行模型。
10.1 核心架构图解:米其林餐厅 vs 外卖平台
为了直观理解两者的差异,我们通过一张架构对比图来展示它们在 调度策略 和 执行并发性 上的根本不同。
#mermaid-svg-3PBZI5efHWeK3pza{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3PBZI5efHWeK3pza .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3PBZI5efHWeK3pza .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3PBZI5efHWeK3pza .error-icon{fill:#552222;}#mermaid-svg-3PBZI5efHWeK3pza .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3PBZI5efHWeK3pza .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3PBZI5efHWeK3pza .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3PBZI5efHWeK3pza .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3PBZI5efHWeK3pza .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3PBZI5efHWeK3pza .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3PBZI5efHWeK3pza .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3PBZI5efHWeK3pza .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3PBZI5efHWeK3pza .marker.cross{stroke:#333333;}#mermaid-svg-3PBZI5efHWeK3pza svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3PBZI5efHWeK3pza p{margin:0;}#mermaid-svg-3PBZI5efHWeK3pza .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-3PBZI5efHWeK3pza .cluster-label text{fill:#333;}#mermaid-svg-3PBZI5efHWeK3pza .cluster-label span{color:#333;}#mermaid-svg-3PBZI5efHWeK3pza .cluster-label span p{background-color:transparent;}#mermaid-svg-3PBZI5efHWeK3pza .label text,#mermaid-svg-3PBZI5efHWeK3pza span{fill:#333;color:#333;}#mermaid-svg-3PBZI5efHWeK3pza .node rect,#mermaid-svg-3PBZI5efHWeK3pza .node circle,#mermaid-svg-3PBZI5efHWeK3pza .node ellipse,#mermaid-svg-3PBZI5efHWeK3pza .node polygon,#mermaid-svg-3PBZI5efHWeK3pza .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3PBZI5efHWeK3pza .rough-node .label text,#mermaid-svg-3PBZI5efHWeK3pza .node .label text,#mermaid-svg-3PBZI5efHWeK3pza .image-shape .label,#mermaid-svg-3PBZI5efHWeK3pza .icon-shape .label{text-anchor:middle;}#mermaid-svg-3PBZI5efHWeK3pza .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3PBZI5efHWeK3pza .rough-node .label,#mermaid-svg-3PBZI5efHWeK3pza .node .label,#mermaid-svg-3PBZI5efHWeK3pza .image-shape .label,#mermaid-svg-3PBZI5efHWeK3pza .icon-shape .label{text-align:center;}#mermaid-svg-3PBZI5efHWeK3pza .node.clickable{cursor:pointer;}#mermaid-svg-3PBZI5efHWeK3pza .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3PBZI5efHWeK3pza .arrowheadPath{fill:#333333;}#mermaid-svg-3PBZI5efHWeK3pza .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3PBZI5efHWeK3pza .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3PBZI5efHWeK3pza .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3PBZI5efHWeK3pza .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3PBZI5efHWeK3pza .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3PBZI5efHWeK3pza .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3PBZI5efHWeK3pza .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3PBZI5efHWeK3pza .cluster text{fill:#333;}#mermaid-svg-3PBZI5efHWeK3pza .cluster span{color:#333;}#mermaid-svg-3PBZI5efHWeK3pza div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3PBZI5efHWeK3pza .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3PBZI5efHWeK3pza rect.text{fill:none;stroke-width:0;}#mermaid-svg-3PBZI5efHWeK3pza .icon-shape,#mermaid-svg-3PBZI5efHWeK3pza .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3PBZI5efHWeK3pza .icon-shape p,#mermaid-svg-3PBZI5efHWeK3pza .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3PBZI5efHWeK3pza .icon-shape rect,#mermaid-svg-3PBZI5efHWeK3pza .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3PBZI5efHWeK3pza .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3PBZI5efHWeK3pza .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3PBZI5efHWeK3pza :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-3PBZI5efHWeK3pza .redisFill>*{fill:#fff3e0!important;stroke:#e65100!important;stroke-width:2px!important;}#mermaid-svg-3PBZI5efHWeK3pza .redisFill span{fill:#fff3e0!important;stroke:#e65100!important;stroke-width:2px!important;}#mermaid-svg-3PBZI5efHWeK3pza .goFill>*{fill:#e1f5fe!important;stroke:#01579b!important;stroke-width:2px!important;}#mermaid-svg-3PBZI5efHWeK3pza .goFill span{fill:#e1f5fe!important;stroke:#01579b!important;stroke-width:2px!important;}#mermaid-svg-3PBZI5efHWeK3pza .component>*{fill:#ffffff!important;stroke:#333!important;stroke-dasharray:5 5!important;}#mermaid-svg-3PBZI5efHWeK3pza .component span{fill:#ffffff!important;stroke:#333!important;stroke-dasharray:5 5!important;}
Go Netpoller (外卖平台模式)
就绪事件
抢占/窃取
抢占/窃取
抢占/窃取
Conn A
Netpoller (Epoll)
Conn B
Global / Local RunQueue
P1 + M1
P2 + M2
P3 + M3
特点: 1. IO 并行, 逻辑并行2. 动态调度 (Work Stealing)3. 需要用户加锁 (Mutex)
Redis 8.4 (米其林餐厅模式)
固定绑定
固定绑定
固定绑定
解析完的命令
解析完的命令
Client A
IO Thread 1
Client B
Client C
IO Thread 2
无锁交换队列
Main Thread (厨师长)
特点: 1. IO 并行, 逻辑串行2. 静态绑定 (Affinity 极高)3. 无锁执行 (No Race)
10.2 核心差异深度解析
10.2.1. 逻辑执行的并发性(Serial vs Parallel)
- Redis 8.4 (IO 并行,逻辑串行):
- 机制:所有 IO 线程将解析好的命令汇聚给主线程,主线程排队执行。
- 目的:Redis 拥有 Hash, ZSet 等复杂数据结构。如果逻辑并行,必须引入细粒度的行级锁(Row-level Lock)。在高频操作下,锁竞争的开销会完全抵消多核的收益。
- 收益:开发者享受了“无锁编程”的红利,原子性天然保证,代码极度简单且高效。
- Go (全并行):
- 机制:任意 Goroutine 可能在任意 P(CPU 核)上运行,业务逻辑天然并行。
- 代价:当多个 Goroutine 操作共享资源(如全局 Map)时,开发者必须显式使用 sync.Mutex 或 Channel。
10.2.2. 调度哲学:静态绑定 vs 工作窃取
- Redis (Static Binding):
- 策略:assignClientToIOThread。一个连接一旦分配给 3 号线程,除非断开,否则永远由 3 号线程负责。
- 优势:极致的 CPU 亲和性 (CPU Affinity) 和 L1/L2 Cache 命中率。Redis 是内存数据库,Cache Miss 是最大的性能杀手。这种设计保证了连接的上下文始终热存在于某个 CPU 核心的缓存中。
- Go (Work Stealing):
- 策略:如果 P1 闲了而 P2 忙,P1 会从 P2 的队列里“偷”任务(G)来跑。
- 优势:最大化 CPU 吞吐量。保证没有 CPU 核心在偷懒。
- 劣势:Goroutine 可能在不同 CPU 核之间漂移,导致缓存失效(Cache Thrashing)。对于通用业务这没问题,但对于 Redis 这种微秒级延迟敏感的应用是不可接受的。
10.3 终极追问:为什么 Redis 不用 Go 重写?
如果用 Go 重写 Redis,理论上网络层代码会减少 90%,但会面临三个无法逾越的障碍:
总结: Redis 8.4 的网络模型,是在 C 语言没有 Goroutine 这种“黑科技”的前提下,通过极致的工程设计,手动实现了一套“专用的、静态绑定的、针对特定场景(内存操作)优化的” GMP 模型。它是特种部队,而 Go 的 Netpoller 是通用集团军。
网硕互联帮助中心

![【LE Audio】BAP协议精讲[4]: 蓝牙LE音频单播服务器实战指南——从服务部署到能力公示的全流程解析-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/01/20260124231625-697552c96d70f-220x150.png)



评论前必须登录!
注册