面试回答
创建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;
}
核心逻辑:
-
主线程调用
epoll_wait阻塞等待就绪事件(无事件时休眠,消耗极低); -
epoll返回就绪的FD和事件,存入
eventLoop->fired数组; -
遍历就绪事件数组,根据事件类型(读/写)调用对应的回调函数:
- 读事件:
readQueryFromClient(读取客户端命令,解析并执行); - 写事件:
sendReplyToClient(将命令执行结果返回给客户端);
- 读事件:
-
循环往复,直到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;
}
四、核心设计亮点(源码层面)
- 抽象层封装 :通过
aeApiCreate/aeApiAddEvent等接口,将epoll/kqueue/select的差异封装,实现跨平台兼容; - 数组映射FD :
eventLoop->events数组下标直接对应FD,无需遍历查找,通过FD能O(1)找到对应的事件对象; - 非阻塞IO :Redis在读写FD时均设置为非阻塞模式(
fcntl(fd, F_SETFL, O_NONBLOCK)),即使epoll通知就绪,读写也不会阻塞; - 事件复用:读/写事件可独立注册/删除,比如执行完命令后注册写事件,返回结果后删除写事件,避免无效触发。
总结
- Redis IO多路复用的核心是aeEventLoop事件循环框架,封装了epoll等底层多路复用接口,对外提供统一的事件注册/处理能力;
- 源码层面的核心流程:初始化事件循环→注册FD事件→epoll_wait等待就绪→遍历就绪事件→调用回调处理→循环往复;
- 关键设计:FD与事件数组下标映射(O(1)查找)、跨平台抽象层、非阻塞IO,保证了单线程处理海量连接的高效性。
这些源码逻辑也是Redis单线程能支撑10万+ QPS的核心------所有开销都集中在"处理就绪事件",无线程切换、无锁竞争,且内存操作极快。