Redis的IO多路复用

面试回答

创建epoll实例,每一个客户端(socket)都对应一个FD(文件描述符),将这些FD注册到epoll中,并为FD绑定回调函数,主线程通过epoll_await()获取等待就绪事件,epoll返回就绪的FD和事件,内核将就绪的 FD 拷贝到用户态。redis主线程逐一处理(其实就是一个死循环)。

可以讲讲select、poll、epoll

为什么redis 6.0 之后网络IO的处理改成多线程的方式了

  • 第一个缺点:因为只有一个进程,无法充分利用 多核 CPU 的性能
  • 第二个缺点:handler对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;

什么是handler 在单 Reactor 单线程模型中,Handler(事件处理器)在处理某个连接的业务逻辑时,会独占整个线程 。 因为 Redis 6.0 之前的网络模型是单线程的,所以当 Handler 正在处理一个耗时操作时,其他连接的事件(读、写、accept)都无法被处理,从而导致整体响应变慢。

Redis为了跨平台兼容,将不同系统的多路复用接口(epoll/kqueue/select等)封装成了统一的aeEventLoop (事件循环)框架,核心源码集中在ae.c/ae.h和对应系统的实现文件(如ae_epoll.c/ae_kqueue.c)中。

下面我会从核心数据结构、初始化流程、事件注册、事件等待/处理四个维度,结合关键源码片段拆解Redis IO多路复用的实现。

一、先明确核心源码文件

Redis的IO多路复用核心代码在以下文件(Redis 6.x/7.x版本):

  • src/ae.h:定义事件循环、事件对象的核心数据结构;
  • src/ae.c:事件循环的通用逻辑(跨平台);
  • src/ae_epoll.c:Linux下epoll的具体实现;
  • src/ae_kqueue.c:macOS/FreeBSD下kqueue的实现;
  • src/ae_select.c:兼容select/poll的实现。

二、核心数据结构(ae.h)

Redis先定义了统一的抽象层,屏蔽不同多路复用器的差异,核心结构如下:

1. 事件对象(aeFileEvent)

表示一个文件描述符(FD)对应的监听事件(读/写):

c 复制代码
// ae.h
typedef struct aeFileEvent {
    // 事件类型:AE_READABLE(读事件) / AE_WRITABLE(写事件)
    int mask; 
    // 读事件回调函数(客户端发命令时触发)
    aeFileProc *rfileProc;
    // 写事件回调函数(给客户端返回结果时触发)
    aeFileProc *wfileProc;
    // 私有数据(通常指向客户端连接结构client)
    void *clientData;
} aeFileEvent;
2. 就绪事件(aeFiredEvent)

存储epoll返回的"就绪FD+事件",供后续处理:

c 复制代码
// ae.h
typedef struct aeFiredEvent {
    int fd;     // 就绪的文件描述符
    int mask;   // 就绪的事件类型(AE_READABLE/AE_WRITABLE)
} aeFiredEvent;
3. 事件循环(aeEventLoop)

整个IO多路复用的核心上下文,管理所有监听事件和就绪事件:

c 复制代码
// ae.h
typedef struct aeEventLoop {
    int maxfd;                          // 当前监听的最大FD
    int setsize;                        // 最大支持的FD数量(可配置)
    long long timeEventNextId;          // 时间事件ID(定时任务用)
    time_t lastTime;                    // 最后一次处理时间事件的时间
    aeFileEvent *events;                // 所有注册的文件事件(数组,下标=FD)
    aeFiredEvent *fired;                // 就绪的文件事件(数组)
    aeTimeEvent *timeEventHead;         // 时间事件链表(定时任务)
    int stop;                           // 事件循环停止标志
    void *apidata;                      // 多路复用器私有数据(如epoll的fd)
    aeBeforeSleepProc *beforesleep;     // 事件循环休眠前的回调
    aeBeforeSleepProc *aftersleep;      // 事件循环唤醒后的回调
} aeEventLoop;

三、核心流程(源码级拆解)

以Linux下epoll实现为例,拆解Redis IO多路复用的完整流程:

1. 事件循环初始化(aeCreateEventLoop)
c 复制代码
// ae.c
aeEventLoop *aeCreateEventLoop(int setsize) {
    aeEventLoop *eventLoop = zmalloc(sizeof(aeEventLoop));
    eventLoop->setsize = setsize;
    eventLoop->maxfd = -1;
    // 初始化事件数组:下标=FD,存储该FD的监听事件
    eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
    // 初始化就绪事件数组:存储epoll返回的就绪事件
    eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
    eventLoop->stop = 0;
    eventLoop->lastTime = time(NULL);
    
    // 初始化具体的多路复用器(epoll)
    // aeApiCreate会调用epoll_create()创建epoll实例
    if (aeApiCreate(eventLoop) == -1) {
        zfree(eventLoop->events);
        zfree(eventLoop->fired);
        zfree(eventLoop);
        return NULL;
    }
    return eventLoop;
}

// ae_epoll.c:epoll的初始化实现
static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));
    // 创建epoll实例,返回epoll_fd
    state->epfd = epoll_create(1024); // 1024是历史参数,现在无意义
    eventLoop->apidata = state;
    return (state->epfd == -1) ? -1 : 0;
}

核心逻辑

  • 创建事件循环对象,初始化事件数组(监听事件)和就绪事件数组;
  • 调用aeApiCreate创建epoll实例,返回的epoll_fd存储在eventLoop->apidata中。
2. 注册事件(aeCreateFileEvent)

当Redis监听端口(6379)或新建客户端连接时,会注册FD的监听事件:

c 复制代码
// ae.c
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    aeFileEvent *fe = &eventLoop->events[fd];

    // 调用epoll_ctl注册事件到epoll实例
    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    
    // 设置事件的回调函数和私有数据
    fe->mask |= mask;
    if (mask & AE_READABLE) fe->rfileProc = proc;
    if (mask & AE_WRITABLE) fe->wfileProc = proc;
    fe->clientData = clientData;
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;
    return AE_OK;
}

// ae_epoll.c:epoll添加事件的实现
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0}; // epoll_event结构体
    // 获取该FD已注册的事件(避免重复注册)
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    // 转换Redis事件掩码为epoll事件掩码
    ee.events = 0;
    mask |= eventLoop->events[fd].mask; // 合并已有事件
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;

    // 调用epoll_ctl注册/修改事件
    if (epoll_ctl(state->epfd, op, fd, &ee) == -1) return -1;
    return 0;
}

核心逻辑

  • 将FD和监听事件(读/写)注册到epoll实例(epoll_ctl的EPOLL_CTL_ADD/EPOLL_CTL_MOD);
  • 为该FD绑定回调函数(比如读事件回调readQueryFromClient,处理客户端命令)。
3. 事件循环(aeMain)

Redis启动后,主线程进入无限循环,等待并处理就绪事件:

c 复制代码
// ae.c
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    // 无限循环,直到stop被置1(Redis关闭)
    while (!eventLoop->stop) {
        // 休眠前的回调(比如处理定时任务、统计)
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        // 核心:等待就绪事件,返回就绪的事件数量
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

// ae.c:处理事件的核心函数
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
    int processed = 0, numevents;

    // 调用aeApiPoll(epoll_wait)等待就绪事件
    numevents = aeApiPoll(eventLoop, tvp);
    processed += numevents;

    // 遍历所有就绪事件,逐个处理
    for (int j = 0; j < numevents; j++) {
        aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
        int mask = eventLoop->fired[j].mask;
        int fd = eventLoop->fired[j].fd;
        int rfired = 0;

        // 处理读事件
        if (fe->mask & mask & AE_READABLE) {
            rfired = 1;
            // 调用读事件回调(比如readQueryFromClient)
            fe->rfileProc(eventLoop, fd, fe->clientData, mask);
        }
        // 处理写事件
        if (fe->mask & mask & AE_WRITABLE) {
            if (!rfired || fe->wfileProc != fe->rfileProc) {
                // 调用写事件回调(比如sendReplyToClient)
                fe->wfileProc(eventLoop, fd, fe->clientData, mask);
            }
        }
        processed++;
    }
    // 处理时间事件(定时任务,如过期键清理)
    ...
    return processed;
}

// ae_epoll.c:epoll_wait等待就绪事件
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval;
    // 调用epoll_wait,阻塞等待就绪事件,结果存入eventLoop->fired
    retval = epoll_wait(state->epfd, eventLoop->fired, eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
    return retval;
}

核心逻辑

  1. 主线程调用epoll_wait阻塞等待就绪事件(无事件时休眠,消耗极低);

  2. epoll返回就绪的FD和事件,存入eventLoop->fired数组;

  3. 遍历就绪事件数组,根据事件类型(读/写)调用对应的回调函数:

    1. 读事件:readQueryFromClient(读取客户端命令,解析并执行);
    2. 写事件:sendReplyToClient(将命令执行结果返回给客户端);
  4. 循环往复,直到Redis停止。

4. 事件删除(aeDeleteFileEvent)

当客户端断开连接时,Redis会删除该FD的监听事件:

c 复制代码
// ae.c
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask) {
    if (fd >= eventLoop->setsize) return;
    aeFileEvent *fe = &eventLoop->events[fd];
    if (fe->mask == AE_NONE) return;

    // 从epoll中删除事件
    aeApiDelEvent(eventLoop, fd, mask);
    // 更新事件掩码
    fe->mask = fe->mask & (~mask);
    if (fd == eventLoop->maxfd && fe->mask == AE_NONE) {
        // 更新maxfd,优化后续遍历
        while (eventLoop->maxfd >= 0 && eventLoop->events[eventLoop->maxfd].mask == AE_NONE)
            eventLoop->maxfd--;
    }
}

// ae_epoll.c
static int aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0};
    int mask = eventLoop->events[fd].mask & (~delmask);

    ee.events = 0;
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;
    int op = mask == AE_NONE ? EPOLL_CTL_DEL : EPOLL_CTL_MOD;
    // 调用epoll_ctl删除/修改事件
    if (epoll_ctl(state->epfd, op, fd, &ee) == -1) return -1;
    return 0;
}

四、核心设计亮点(源码层面)

  1. 抽象层封装 :通过aeApiCreate/aeApiAddEvent等接口,将epoll/kqueue/select的差异封装,实现跨平台兼容;
  2. 数组映射FDeventLoop->events数组下标直接对应FD,无需遍历查找,通过FD能O(1)找到对应的事件对象;
  3. 非阻塞IO :Redis在读写FD时均设置为非阻塞模式(fcntl(fd, F_SETFL, O_NONBLOCK)),即使epoll通知就绪,读写也不会阻塞;
  4. 事件复用:读/写事件可独立注册/删除,比如执行完命令后注册写事件,返回结果后删除写事件,避免无效触发。

总结

  1. Redis IO多路复用的核心是aeEventLoop事件循环框架,封装了epoll等底层多路复用接口,对外提供统一的事件注册/处理能力;
  2. 源码层面的核心流程:初始化事件循环→注册FD事件→epoll_wait等待就绪→遍历就绪事件→调用回调处理→循环往复;
  3. 关键设计:FD与事件数组下标映射(O(1)查找)、跨平台抽象层、非阻塞IO,保证了单线程处理海量连接的高效性。

这些源码逻辑也是Redis单线程能支撑10万+ QPS的核心------所有开销都集中在"处理就绪事件",无线程切换、无锁竞争,且内存操作极快。

相关推荐
2501_916766542 小时前
【SpringMVC】异常处理和拦截器
java·spring
不惑_2 小时前
在 Docker 中运行 Java JAR 包实战教程
java·docker·jar
一勺菠萝丶2 小时前
解决Java中IP地址访问HTTPS接口的SSL证书验证问题
java·tcp/ip·https
墨着染霜华2 小时前
IntelliJ IDEA 设置导出与导入完整指南(备份 / 迁移 / 团队共享)
java·ide·intellij-idea
浮游本尊2 小时前
Java学习第32天 - 性能优化与架构设计
java
卡尔特斯2 小时前
Windows Redis Memurai 配置指南、用户创建、权限管理
redis
五阿哥永琪2 小时前
Nacos注册/配置中心
java·开发语言
无敌最俊朗@2 小时前
Qt多线程阻塞:为何信号失效?
java·开发语言