云计算百科
云计算领域专业知识百科平台

万字长文解析:Redis 8.4 网络 IO 架构深度拆解

第一部分: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 的核心时序:

  • BeforeSleep:在进入 epoll_wait 阻塞之前,处理一些“顺带”的任务。例如:处理已经解析完命令的 Client 读写队列(Threaded IO 相关)、同步 AOF 到磁盘、推送集群状态等。
  • aeApiPoll:调用 epoll_wait(或 io_uring 等),主线程在此处阻塞。
  • AfterSleep:从 epoll_wait 唤醒后立即执行的任务。
  • 处理就绪事件:遍历 fired 数组,根据事件类型(READ/WRITE)执行相应的回调函数(如 readQueryFromClient 或 sendReplyToClient)。
  • 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(并行解析/并行写缓冲)


    图表说明(可作为博文注释):

  • 解耦设计 (PrivData):图中特别标注了 Redis 8.4 引入的 privdata[0] 赋值逻辑。这在源码分析中是一个关键点,它标志着 ae 事件库开始尝试脱离对 server 全局变量的强依赖。
  • 抽象层 (Connection Layer):在 listenToPort 与 acceptTcpHandler 之间,Redis 并不是直接操作原始 FD,而是通过 connection 结构体进行封装。这层“虚函数表”设计使得 Redis 8.4 能在不改动核心逻辑的情况下支持 TLS 或不同的内核后端(如 io_uring)。
  • 核心循环 (EventLoop):
    • 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 Mask)Linux (epoll) 映射结果BSD (kqueue) 映射结果io_uring 映射结果
    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。但这些方案在高并发(每秒百万级请求)下都面临着同样的阿喀琉斯之踵:

  • 频繁的系统调用(System Call)开销:每次调用 epoll_ctl 或 epoll_wait,CPU 都要从“用户态”切换到“内核态”,这种上下文切换非常昂贵。
  • 数据拷贝:内核需要将事件从内核空间拷贝到用户空间缓冲区。
  • 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 模型:

  • 任务分发 (SQ):当 Redis 调用 aeApiAddEvent 时,它不再立即执行耗时的系统调用,而是构建一个 sqe(提交队列条目)放入 SQ 中。在 Redis 8.4 的单次事件循环中,多个事件变更可以积压在一起,通过一次 io_uring_submit 批量交给内核。
  • 就绪获取 (CQ):在 aeApiPoll 中,Redis 直接检查 CQ 环形缓冲区。如果内核已经填充了就绪事件,Redis 通过简单的指针偏移即可读取结果。这一步在理想情况下是 “零系统调用” 的。
  • 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 网络优化要点总结

  • 防御性编程:在 8.4 源码中,每次读回调执行后必刷新 fe 指针,这是应对复杂多线程环境下 FD 动态销毁的坚固防线。
  • 调度精准性:通过 AE_BARRIER 与 beforesleep 的联动,Redis 证明了即使是单线程 Reactor,也能通过精细的顺序控制榨干硬件性能。
  • 第三部分:连接层的抽象设计(Connection Layer)

    3.1 演进背景:从“原始 FD”到“传输层无关”

    在 Redis 6.0 之前,Redis 直接操作文件描述符(FD),读写逻辑直接耦合 read() 和 write() 系统调用。但随着 Redis 6.0 原生支持 TLS(加密传输)以及 8.4 对复杂网络环境的适配,这种“硬编码”模式难以为继。

    3.1.1 为什么需要 connection 抽象层?

  • 屏蔽 TLS 复杂性:普通 TCP 只需 read(),但 TLS 需要经过复杂的解密和状态处理(如 SSL_read())。
  • 异步握手管理:TLS 连接在 TCP 三次握手后,还需要进行多次密钥交换(Handshake)。在非阻塞 IO 模式下,握手过程是碎片化的,必须有一个状态机来管理。
  • 多态支持:通过虚函数表,让业务逻辑(networking.c)无需关心底层是纯文本还是加密流。
  • 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 握手流转过程

  • 监听触发:acceptTcpHandler 接收到新连接,调用 connCreateAccept()。
  • 挂起业务:如果是 TLS 连接,状态设为 CONN_STATE_ACCEPTING。此时 不会 创建 client 对象。
  • 注册握手回调:底层调用 aeCreateFileEvent 注册读事件,但回调指向 tlsHandshakeHandler。
  • 分片执行:
    • ae 触发读事件 -> 进入握手处理器。
    • 调用 SSL_do_handshake()。
    • 如果数据不足(WANT_READ),直接返回 ae 循环,等待下一次触发。
  • 激活业务:直到握手成功,状态转为 CONNECTED,此时才调用 accept_handler(即 createClient),正式进入 Redis 命令处理流程。
  • 结论:这种设计确保了业务逻辑层永远只能读到“已经解密好”的清洁字节流。

    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 在连接层对内核参数进行了微操:

  • O_NONBLOCK (非阻塞映射): 所有连接在建立瞬间即通过 anetNonBlock 设置。这保证了 aeApiPoll 唤醒后,任何 read/write 都能立即返回而非挂起主线程。
  • TCP_NODELAY (延迟克星): Redis 默认禁用 Nagle 算法。虽然这会略微降低带宽利用率,但能大幅减少小包请求的平均响应时间(RTT),这符合 Redis 内存数据库的定位。
  • SO_KEEPALIVE: 由 server.tcpkeepalive 参数控制。连接层通过定时发送探针,清理那些因为网络波动、防火墙超时而导致的“半打开连接(Half-Open Connections)”,防止 client 对象泄露。
  • 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 安全性的体现:

  • 参数校验:SET 命令参数必须

    3

    \\ge 3

    3

  • ACL 权限:检查当前 User 是否有 SET 权限,以及对 Key name 是否有写权限。
  • 内存状态:若是写命令且 maxmemory 已满,立即触发 performEvictions。
  • 执行:/* 核心调用 */
    c->cmd->proc(c); // 此时跳转至 setCommand 函数
    setCommand 随后调用底层 dbAdd 函数,将 "name" 和 "molaifeng" 写入全局哈希表。
  • 4.5 案例总结:SET name molaifeng 的全旅程

  • 流入:内核收到数据 -> ae 触发 readQueryFromClient -> (若开启 TIO)放入 Pending 队列。
  • 缓冲:数据被写入 querybuf,如果缓冲区不够大,SDS 自动扩容。
  • 拆解:状态机逐字节扫描,将 *3… 拆解为 3 个 robj 对象存入 c->argv。
  • 寻径:lookupCommand 在 O(1) 时间内从哈希表找到 SET 指令。
  • 落地:经过 ACL 和内存校验,主线程执行 setCommand,数据存入内存字典。
  • 反馈:addReply 将 +OK\\r\\n 写入客户端输出缓冲区,等待发送。
  • 这种设计确保了 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 线程。

    流程逻辑:

  • 迎宾:主线程 accept 新连接。
  • 过户:主线程将连接从自己的 epoll 中移除(Unbind),并在内存中标记“这个连接归 3 号线程管”。
  • 接管:3 号线程被唤醒,将连接绑定到自己的 epoll 中(Rebind)。
  • 源码实证 (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)推送等副作用。

    核心流程:

  • 查找命令:在 processCommand 中,通过 lookupCommand 找到对应的 redisCommand 结构体(如 setCommand)。
  • 多态调用:call() 函数通过函数指针执行具体逻辑:/* src/server.c */
    c->cmd->proc(c);
  • 原子性上下文:在 call() 执行期间,主线程被当前 Client 独占。这意味着命令执行是串行的、原子的,不会发生并发数据竞争(Race Condition)。
  • 数据生产:具体的命令函数(如 getCommand)从数据库查找到数据后,必须调用 addReply 系列函数。这是数据从“数据库层”流向“网络层”的唯一入口。
  • 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 吞吐量飙升的秘诀。

  • 移交:主线程调用 assignClientToIOThread,将 Client 放入 IO 线程的队列,并唤醒 IO 线程。
  • 并发写:IO 线程在 processClientsFromMainThread 中接管 Client,调用 writeToClient。
  • 系统调用:writev 系统调用在 Worker 线程中并发执行。这意味着如果带宽打满,阻塞的是 Worker 线程,主线程依然可以处理其他逻辑。
  • 路径 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 执行了严格的资源解绑顺序,防止悬挂指针:

  • 从 IO 线程剥离:如果 Client 还挂在 IO 线程的 EventLoop 上,强制解绑。
  • 取消订阅:解绑 Pub/Sub 字典。
  • 释放缓冲区:释放 querybuf、c->buf、c->reply 链表。
  • 关闭 Socket:最后才关闭文件描述符。
  • 这种“标记-异步回收-批量释放”的策略,确保了 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%,但会面临三个无法逾越的障碍:

  • GC 的“停顿之殇”: Redis 经常承载几十 GB 的数据。Go 的 GC 虽然在优化,但在扫描巨大堆内存时产生的 STW(或写屏障带来的吞吐下降),对于要求 99.9% 响应在 1ms 以内 的缓存服务来说是致命的。
  • 内存布局的“黑盒”: C 语言允许 Redis 手动控制内存布局(如 SDS 的紧凑内存、Ziplist 的连续内存),以利用 CPU Cache Line。Go 的内存由 Runtime 托管,无法做到如此极致的微调。
  • 锁的“并发陷阱”: Go 的高并发模型需要锁来保护全局状态。在 Redis 的场景下(全是全局状态),锁的开销 > 单线程顺序执行的开销。
  • 总结: Redis 8.4 的网络模型,是在 C 语言没有 Goroutine 这种“黑科技”的前提下,通过极致的工程设计,手动实现了一套“专用的、静态绑定的、针对特定场景(内存操作)优化的” GMP 模型。它是特种部队,而 Go 的 Netpoller 是通用集团军。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 万字长文解析:Redis 8.4 网络 IO 架构深度拆解
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!