Redis网络模型-基于epoll的服务器端流程

一、前言:从启动到响应的全景图

在前面的文章中,我们已经分别探讨了 selectpollepoll 的原理,以及 epoll 中 LT/ET 模式的区别。现在,是时候将这些知识串联起来,完整地描绘出 Redis 服务器端基于 epoll 的工作全景图

Redis 的高性能并非来自单一的"银弹",而是其精巧的单线程 + I/O 多路复用 + 高效数据结构 三位一体架构的完美体现。其中,epoll 作为 Linux 平台上的 I/O 多路复用器,扮演着"中枢神经"的角色,负责高效地收集和分发所有网络事件。

💡 核心价值
理解 Redis 基于 epoll 的完整工作流程,不仅能让你掌握一个顶级开源项目的核心设计,更能为你构建自己的高性能网络服务提供绝佳的范本

本文将带你:

  • main 函数开始,一步步追踪 Redis 的启动流程
  • 剖析其事件循环 (aeEventLoop) 如何驱动整个服务器
  • 详解客户端连接建立、命令处理、结果返回的全过程

二、服务器启动:万事俱备,只欠东风

2.1 初始化阶段 (main -> initServer)

当执行 redis-server 命令后,程序从 main 函数进入,最终会调用 initServer() 函数进行核心初始化。

cpp 复制代码
// server.c
int main(int argc, char **argv) {
    // ...
    initServer(); // 核心初始化
    // ...
    aeMain(server.el); // 进入主事件循环
}

2.2 创建监听套接字 (Listening Socket)

initServer 的首要任务之一就是创建一个用于监听客户端连接请求的 TCP 套接字。

cpp 复制代码
void initServer(void) {
    // ...
    // 1. 创建 TCP socket
    server.fd = anetTcpServer(server.neterr, server.port, server.bindaddr, server.protected_mode);
    // ...
}

这个 server.fd 就是我们的监听套接字,它被绑定到配置文件中指定的端口(默认 6379)。

2.3 创建 epoll 实例与事件循环

接下来,Redis 会初始化其核心的事件驱动框架。

cpp 复制代码
void initServer(void) {
    // ...
    // 2. 创建事件循环 (aeEventLoop)
    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
    // ...
}

在 Linux 系统上,aeCreateEventLoop 内部会调用 aeApiCreate,而后者最终会执行 epoll_create1(EPOLL_CLOEXEC),创建一个 epoll 实例,并将其文件描述符 (epfd) 保存在 aeApiState 结构中。

2.4 注册监听事件

万事俱备,只差将监听套接字注册到 epoll 实例中。

cpp 复制代码
void initServer(void) {
    // ...
    // 3. 将监听套接字 server.fd 注册到事件循环中
    if (aeCreateFileEvent(server.el, server.fd, AE_READABLE,
        acceptTcpHandler, NULL) == AE_ERR) {
        // 错误处理
    }
    // ...
}
  • aeCreateFileEvent: Redis 事件循环的 API,用于注册文件事件。
  • AE_READABLE: 表示关心"可读"事件。
  • acceptTcpHandler: 事件处理器函数。当监听套接字变为可读(即有新连接到达)时,该函数会被调用。

这一步内部会调用 epoll_ctl(EPOLL_CTL_ADD, ...),将 server.fd 添加到 epoll 的红黑树中,并关联 acceptTcpHandler 回调。

至此,Redis 服务器已成功启动,并进入了"待命"状态,静静地等待客户端的到来。


三、主事件循环:永不停止的心脏

服务器启动后,main 函数会调用 aeMain(server.el),进入主事件循环。

cpp 复制代码
// ae.c
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        // 核心:处理文件事件和时间事件
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

aeProcessEvents 是整个事件循环的核心,它主要做两件事:

  1. 计算超时时间 :如果有定时任务(如 serverCron),则计算下次执行前需要等待的时间。
  2. 调用 aeApiPoll :这是最关键的一步,它会阻塞在 epoll_wait 上,等待内核通知事件发生。
cpp 复制代码
// ae_epoll.c
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    // ...
    // 调用 epoll_wait,阻塞等待事件
    numevents = epoll_wait(state->epfd, state->events, eventLoop->setsize, timeout);
    // ...
    // 将内核返回的就绪事件填充到 eventLoop->fired 数组
    for (j = 0; j < numevents; j++) {
        eventLoop->fired[j].fd = state->events[j].data.fd;
        // ... 设置 mask
    }
    return numevents;
}

关键点epoll_wait 返回后,eventLoop->fired 数组里只包含真正就绪的 fd。主循环接下来只需要遍历这个数组即可,效率极高。


四、处理客户端请求:从连接到响应

4.1 接受新连接 (acceptTcpHandler)

当第一个客户端尝试连接时,内核会将监听套接字 server.fd 加入 epoll 的就绪链表。epoll_wait 返回后,Redis 会调用 acceptTcpHandler

cpp 复制代码
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cfd, max = MAX_ACCEPTS_PER_CALL;
    while(max--) {
        // 1. 调用 accept 接受新连接
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) break;
        
        // 2. 为新客户端创建 client 对象
        acceptCommonHandler(createClient(cfd), 0, cip);
    }
}

createClient 函数会为这个新连接分配一个 client 结构体,并将新客户端的套接字 cfd 也注册到 epoll 实例中 ,关心其 AE_READABLE 事件,并设置处理器为 readQueryFromClient

cpp 复制代码
// networking.c
client *createClient(int fd) {
    // ...
    if (fd != -1) {
        // 将新客户端的 fd 注册到 epoll,关心可读事件
        aeCreateFileEvent(server.el, fd, AE_READABLE, readQueryFromClient, c);
    }
    return c;
}

4.2 读取并解析命令 (readQueryFromClient)

当客户端发送命令(如 SET key value)时,其套接字 cfd 变为可读,epoll_wait 返回,并触发 readQueryFromClient

cpp 复制代码
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    client *c = (client*) privdata;
    // 1. 从 socket 读取数据到 client 的 querybuf 缓冲区
    nread = read(fd, c->querybuf+qblen, readlen);
    
    // 2. 解析缓冲区中的 RESP 协议,生成 redisCommand
    processInputBuffer(c);
}

processInputBuffer 会解析 RESP 协议,并将完整的命令放入 clientcmd 字段中。

4.3 执行命令并返回结果

processInputBuffer 的末尾,会调用 processCommandAndResetClient 来执行命令。

cpp 复制代码
void processCommandAndResetClient(client *c) {
    // 1. 执行具体的命令逻辑 (如 setCommand, getCommand)
    if (processCommand(c) == C_OK) {
        // 2. 如果客户端有数据要写回(通常是命令执行结果)
        if (clientHasPendingReplies(c)) {
            // 将该客户端的 fd 注册为可写事件
            aeCreateFileEvent(server.el, c->fd, AE_WRITABLE, sendReplyToClient, c);
        }
    }
}

4.4 发送响应 (sendReplyToClient)

一旦客户端的套接字变为可写(通常很快),epoll_wait 会再次返回,并触发 sendReplyToClient

cpp 复制代码
void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    client *c = privdata;
    // 1. 将 client->reply 链表中的数据通过 write/send 写回 socket
    nwritten = write(fd, c->buf + c->sentlen, objlen - c->sentlen);
    
    // 2. 如果所有数据都已发送完毕
    if (c->sentlen == c->bufpos) {
        // 取消对该 fd 的可写事件监听
        aeDeleteFileEvent(server.el, c->fd, AE_WRITABLE);
    }
}

至此,一次完整的"连接 -> 请求 -> 响应"流程圆满完成。


五、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
这个DBA有点耶1 小时前
MySQL深分页优化:从LIMIT 1000000,10到毫秒级响应的三种写法
数据库·程序人生·mysql·性能优化·学习方法·dba·改行学it
通往曙光的路上1 小时前
mysql3
数据库
heimeiyingwang1 小时前
【架构实战】容器网络CNI:让Pod与Pod、Pod与外界自由通信
网络·架构
阿坤带你走近大数据1 小时前
什么是 REDO LOG,它在 Oracle 数据库中的作用是什么?
数据库·oracle
东风破1371 小时前
DM8搭建同构(dm-dm)及异构数据库(dm-oracle,dm-mysql)的dblink
数据库·mysql·oracle
无限进步_2 小时前
【Linux】网络发展背景与协议分层模型
linux·运维·网络
凭X而动2 小时前
postgresql18.1部署
数据库·postgresql
万邦科技Lafite2 小时前
京东商品详情 API 接口全面讲解
java·数据库·redis·api·电商开放平台
无风听海2 小时前
MongoDB GridFS 一些处理细节解析
数据库·mongodb