redis-源码带读-02-事件驱动与网络模型
Redis 源码带读 第2篇:事件驱动与网络模型
本篇目标
理解Redis单线程事件循环的核心机制,包括ae事件抽象层、epoll实现、网络处理和客户端生命周期。
前置知识
- 了解epoll的基本原理
- 了解非阻塞I/O和I/O多路复用
- 阅读过第1篇
1. ae.c 事件循环抽象层
1.1 核心数据结构
// ae.h:79
typedef struct aeEventLoop {
int maxfd; // 已注册的最大文件描述符
int setsize; // 最大文件描述符数
long long timeEventNextId; // 下一个时间事件ID
aeFileEvent *events; // 文件事件数组(按fd索引)
aeFiredEvent *fired; // 已触发事件数组
aeTimeEvent *timeEventHead; // 时间事件链表头(无序)
int stop; // 是否停止
int nevents; // 事件数组大小(惰性增长)
void *apidata; // 平台特定数据(epoll fd等)
aeBeforeSleepProc *beforesleep; // 睡眠前回调
aeBeforeSleepProc *aftersleep; // 睡眠后回调
} aeEventLoop;
1.2 文件事件(File Event)
// ae.h:52
typedef struct aeFileEvent {
int mask; // AE_READABLE | AE_WRITABLE | AE_BARRIER
aeFileProc *rfileProc; // 读回调
aeFileProc *wfileProc; // 写回调
void *clientData;
} aeFileEvent;
1.3 时间事件(Time Event)
// ae.h:60
typedef struct aeTimeEvent {
long long id; // 事件ID
monotime when; // 触发时间(微秒,单调时钟)
aeTimeProc *timeProc; // 回调函数
struct aeTimeEvent *prev; // 双向链表
struct aeTimeEvent *next;
int refcount; // 引用计数(防止递归释放)
} aeTimeEvent;
1.4 核心函数
| 函数 | 行数 | 作用 |
|---|---|---|
aeCreateEventLoop(setsize) |
47 | 创建事件循环 |
aeCreateFileEvent(el,fd,mask,proc,data) |
145 | 注册文件事件 |
aeDeleteFileEvent(el,fd,mask) |
176 | 删除文件事件 |
aeCreateTimeEvent(el,milliseconds,proc,data,finalizerProc) |
218 | 创建时间事件 |
aeMain(el) |
497 | 进入事件循环 |
aeProcessEvents(el,flags) |
365 | 处理就绪事件 |
1.5 事件循环优化
Redis对事件循环进行了多项优化:
- 惰性增长:事件数组初始只分配1024个槽位,按需增长
- 时间事件前置插入:O(1)插入到链表头部
- 懒删除:时间事件删除只标记ID为
AE_DELETED_EVENT_ID,在处理时清理 - 内存预分配:使用
zmalloc_usable利用jemalloc的size class
2. 主循环流程
2.1 aeMain()
// ae.c:497
void aeMain(aeEventLoop *eventLoop) {
while (!eventLoop->stop) {
// 1. 睡眠前处理(刷新缓冲区、处理待关闭客户端等)
if (eventLoop->beforesleep)
eventLoop->beforesleep(eventLoop);
// 2. 处理事件(epoll_wait + 回调)
aeProcessEvents(eventLoop, AE_ALL_EVENTS | AE_CALL_BEFORE_SLEEP | AE_CALL_AFTER_SLEEP);
}
}
2.2 aeProcessEvents()
// ae.c:365
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
int processed = 0;
// 1. 调用beforesleep
if (eventLoop->beforesleep && (flags & AE_CALL_BEFORE_SLEEP))
eventLoop->beforesleep(eventLoop);
// 2. 计算最近时间事件的超时时间
tvp = usUntilEarliestTimer(eventLoop); // O(N)扫描
// 3. 调用epoll_wait(或kqueue/select)
numevents = aeApiPoll(eventLoop, tvp);
// 4. 调用aftersleep(恢复IO线程、更新时间)
if (eventLoop->aftersleep && (flags & AE_CALL_AFTER_SLEEP))
eventLoop->aftersleep(eventLoop);
// 5. 处理就绪事件
for (j = 0; j < numevents; j++) {
int fd = eventLoop->fired[j].fd;
int mask = eventLoop->fired[j].mask;
// 先处理读事件(可能产生写数据,如同步写回复)
if (fd != -1 && mask & AE_READABLE && !reverse) {
eventLoop->events[fd].rfileProc(eventLoop, fd,
eventLoop->events[fd].clientData, mask);
}
// 再处理写事件
if (fd != -1 && mask & AE_WRITABLE) {
eventLoop->events[fd].wfileProc(eventLoop, fd,
eventLoop->events[fd].clientData, mask);
}
processed++;
}
// 6. 处理时间事件
processTimeEvents(eventLoop);
return processed;
}
2.3 事件处理顺序
Redis的事件处理顺序经过精心设计:
epoll_wait返回
|
+-> 先处理读事件(AE_READABLE)
| -> 可能产生写数据(同步写回复)
|
+-> 再处理写事件(AE_WRITABLE)
| -> 发送缓冲区中的回复
|
+-> 处理时间事件
-> serverCron(过期键删除、统计等)
AE_BARRIER标志:用于从服务器和AOF重写场景,反转读写处理顺序,确保先完成写操作再处理读事件。
3. epoll实现:ae_epoll.c
3.1 平台抽象
ae.c通过函数指针实现平台无关性:
// ae_epoll.c
static int aeApiCreate(aeEventLoop *eventLoop);
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask);
static int aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask);
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp);
static int aeApiFree(aeEventLoop *eventLoop);
static int aeApiResize(aeEventLoop *eventLoop, int setsize);
3.2 epoll系统调用映射
| ae函数 | epoll调用 | 说明 |
|---|---|---|
aeApiCreate |
epoll_create(1024) |
创建epoll实例 |
aeApiAddEvent |
epoll_ctl(ADD) |
注册事件 |
aeApiDelEvent |
epoll_ctl(DEL) |
删除事件 |
aeApiPoll |
epoll_wait() |
等待就绪事件 |
3.3 epoll实现细节
// ae_epoll.c
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
state->events = zmalloc(sizeof(struct epoll_event) * eventLoop->setsize);
state->epfd = epoll_create(1024);
eventLoop->apidata = state;
return 0;
}
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0};
ee.events = 0;
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
// EPOLL_CTL_ADD 或 EPOLL_CTL_MOD
if (state->events[fd].events == 0) {
epoll_ctl(state->epfd, EPOLL_CTL_ADD, fd, &ee);
} else {
epoll_ctl(state->epfd, EPOLL_CTL_MOD, fd, &ee);
}
return 0;
}
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int timeout;
if (tvp != NULL) {
timeout = (tvp->tv_sec * 1000) + (tvp->tv_usec / 1000);
} else {
timeout = -1; // 无限等待
}
// 调用epoll_wait
int nevents = epoll_wait(state->epfd, state->events, eventLoop->setsize, timeout);
// 将epoll事件转换为fired事件
for (int j = 0; j < nevents; j++) {
int mask = 0;
if (state->events[j].events & EPOLLIN) mask |= AE_READABLE;
if (state->events[j].events & EPOLLOUT) mask |= AE_WRITABLE;
if (state->events[j].events & EPOLLERR) mask |= AE_READABLE | AE_WRITABLE;
if (state->events[j].events & EPOLLHUP) mask |= AE_READABLE | AE_WRITABLE;
eventLoop->fired[j].fd = state->events[j].data.fd;
eventLoop->fired[j].mask = mask;
}
return nevents;
}
4. 网络层:networking.c
4.1 客户端生命周期
连接到达
-> acceptTcpHandler() [ae事件回调]
-> connCreateAcceptedSocket()
-> createClient()
-> 分配redisClient结构
-> 初始化查询缓冲区(16KB)
-> aeCreateFileEvent(fd, AE_READABLE, readQueryFromClient)
读取命令
-> readQueryFromClient()
-> read(fd, c->querybuf, ...) 读取数据
-> processInputBuffer(c) 解析RESP协议
-> processMultibulkBuffer() 解析*3\r\n$3\r\nSET\r\n...
-> c->argv[0] = "SET"
-> c->argv[1] = "key"
-> c->argv[2] = "value"
-> processCommand(c) 执行命令
写入回复
-> writeToClient()
-> write(fd, c->buf, c->bufpos) 发送响应
-> 如果写不完:注册AE_WRITABLE事件,下次继续
关闭连接
-> freeClient()
-> aeDeleteFileEvent(fd, ...)
-> 释放querybuf、reply等
-> 从clients列表中移除
4.2 readQueryFromClient()
// networking.c:3982
void readQueryFromClient(connection *conn) {
client *c = connGetPrivateData(conn);
int nread, big_arg = 0;
// 1. 检查是否被IO线程拥有
if (c->running_tid != -1 && c->running_tid != MAIN_THREAD_ID) {
c->pending_read = 1; // 延迟到主线程处理
return;
}
// 2. 可复用查询缓冲区(避免每次都分配)
if (c->querybuf == NULL) {
c->querybuf = sdsnewlen(SDS_NOINIT, PROTO_IOBUF_LEN);
}
// 3. 检查缓冲区大小
querybuf_size = sdsavail(c->querybuf);
if (c->querybuf_peak > querybuf_size) {
querybuf_size = c->querybuf_peak;
}
// 4. 读取数据
nread = connRead(conn, c->querybuf + qblen, readlen);
if (nread == -1) {
if (errno == EAGAIN) return; // 没有更多数据
// 其他错误,关闭连接
freeClientAsync(c);
return;
}
// 5. 更新统计
c->net_input_bytes += nread;
// 6. 解析输入
processInputBuffer(c);
}
4.3 processInputBuffer()
// networking.c:3778
int processInputBuffer(client *c) {
while (sdslen(c->querybuf) - c->qb_pos > 0) {
// 1. 检查是否需要更多数据
if (!c->reqtype) {
// 根据第一个字节判断协议类型
if (c->querybuf[c->qb_pos] == '*') {
c->reqtype = PROTO_REQ_MULTIBULK; // RESP协议
} else {
c->reqtype = PROTO_REQ_INLINE; // 内联协议
}
}
// 2. 解析命令
if (c->reqtype == PROTO_REQ_MULTIBULK) {
if (processMultibulkBuffer(c) != C_OK) break;
} else {
if (processInlineBuffer(c) != C_OK) break;
}
// 3. 执行命令
if (processCommandAndResetClient(c) != C_OK) {
return C_ERR; // 客户端已释放
}
}
// 4. 清理已消费的数据
if (c->qb_pos > 0) {
sdsrange(c->querybuf, c->qb_pos, -1);
c->qb_pos = 0;
}
return C_OK;
}
4.4 writeToClient()
// networking.c:2935
int writeToClient(client *c, int handler_installed) {
ssize_t nwritten = 0;
// 1. 发送缓冲区中的回复
if (c->bufpos > 0) {
nwritten = connWrite(c->conn, c->buf, c->bufpos);
if (nwritten <= 0) {
// 写入失败或缓冲区满
return C_OK;
}
c->bufpos -= nwritten;
memmove(c->buf, c->buf + nwritten, c->bufpos);
}
// 2. 发送reply链表中的回复
while (listLength(c->reply) > 0) {
listNode *ln = listFirst(c->reply);
clientReplyBlock *reply = listNodeValue(ln);
nwritten = connWrite(c->conn, reply->bytes + c->repl_offset, reply->size - c->repl_offset);
if (nwritten <= 0) break;
c->repl_offset += nwritten;
if (c->repl_offset == reply->size) {
listDelNode(c->reply, ln);
c->repl_offset = 0;
}
}
// 3. 检查是否发送完成
if (c->bufpos == 0 && listLength(c->reply) == 0) {
c->reply_bytes = 0;
// 移除写事件
aeDeleteFileEvent(server.el, c->fd, AE_WRITABLE);
// 检查是否需要关闭连接
if (c->flags & CLIENT_CLOSE_AFTER_REPLY) {
freeClient(c);
return C_ERR;
}
}
return C_OK;
}
5. RESP协议解析
5.1 RESP协议格式
*3\r\n -> 3个元素的数组
$3\r\n -> 3字节的字符串
SET\r\n -> "SET"
$3\r\n
key\r\n -> "key"
$5\r\n
hello\r\n -> "hello"
5.2 RESP解析器
resp_parser.c 实现了RESP解析器,将网络字节流解析为命令参数数组。
解析流程:
- 读取第一行,确定类型(*数组、+简单字符串、:整数、$批量字符串、-错误)
- 如果是数组,递归解析每个元素
- 将解析结果存储到
c->argv数组中
5.3 内联协议
Redis也支持内联协议(Inline Protocol),用于telnet等简单客户端:
SET key value\r\n
GET key\r\n
内联协议通过空格分隔参数,比RESP协议简单但不支持二进制数据。
6. 非阻塞I/O
Redis在使用非阻塞I/O:
// anet.c
int anetNonBlock(int fd) {
return anetSetBlock(fd, 1); // 1=非阻塞
}
结合epoll实现I/O多路复用:主线程不阻塞在任何单个socket上,而是同时监控所有连接,只处理就绪的连接。
6.1 非阻塞I/O的优势
- 不阻塞:单个连接的慢速读写不会影响其他连接
- 高效:主线程只在epoll_wait时阻塞,处理事件时完全不阻塞
- 可扩展:可以同时处理数万个连接
6.2 非阻塞I/O的挑战
- 需要处理EAGAIN:读取/写入可能返回EAGAIN,需要稍后重试
- 需要缓冲区管理:数据可能分多次到达,需要拼接
- 需要事件驱动:写入缓冲区满时需要注册写事件,可写时继续发送
7. IO线程(Redis 6.0+)
Redis 6.0引入了多线程I/O,用于网络读写:
7.1 IO线程工作原理
主线程:
-> 检查是否有就绪的IO线程
-> 如果有:从IO线程获取解析好的命令
-> 执行命令
-> 将回复放入IO线程的待发送队列
IO线程(最多128个):
-> 读取客户端数据
-> 解析RESP协议
-> 将解析好的命令放入待执行队列
写操作:
-> 主线程将回复放入IO线程的待发送队列
-> IO线程执行实际的write操作
7.2 IO线程配置
# 启用IO线程
io-threads 4
# 是否启用IO线程读取
io-threads-do-reads yes
7.3 IO线程的限制
- 命令执行仍然是单线程
- IO线程只处理网络读写和RESP解析
- 避免了多线程命令执行的锁竞争问题
8. 本篇小结
| 概念 | 要点 |
|---|---|
| 事件循环 | aeMain -> aeProcessEvents -> aeApiPoll(epoll_wait) |
| 文件事件 | fd + mask + 读写回调,epoll管理 |
| 时间事件 | serverCron,每秒10次定时任务 |
| 网络层 | accept -> read -> process -> write -> free |
| RESP协议 | 简单文本协议,*N\r\n$len\r\ndata\r\n |
| 平台抽象 | 函数指针切换epoll/kqueue/select |
| IO线程 | Redis 6.0+,多线程I/O,单线程执行 |
思考题
- 为什么Redis使用单线程处理命令而不是多线程?(提示:锁竞争、数据结构简化)
- serverCron为什么默认是每秒10次而不是1次或100次?
- 如果客户端发送了一个非常大的命令(1GB),Redis会如何处理?
- epoll的ET(边缘触发)和LT(水平触发)模式对Redis有什么影响?
思考题解答
1. 为什么Redis使用单线程处理命令,而不是多线程?
单线程的优势:
- 命令串行化:单线程执行所有命令,天然保证操作的原子性和顺序性,不需要加锁。
- 数据结构简化:不需要考虑线程安全,数据结构设计更简单高效。
- 避免锁竞争:多线程需要细粒度的锁,锁竞争会严重影响性能。
- 无上下文切换:单线程没有线程切换开销。
多线程的问题:
- 锁竞争:共享数据结构需要加锁,锁粒度难以把握
- 死锁风险:多线程环境下死锁难以调试
- 内存屏障:需要使用内存屏障保证可见性,增加复杂度
Redis 6.0的改进:引入多线程I/O,但命令执行仍然是单线程。多线程只用于网络读写(解析和序列化RESP协议),不用于命令处理。
2. serverCron为什么默认每秒运行10次(1次100ms)?
原因:
- 及时性:100ms的间隔足够及时地检测和处理各种状态(过期键、统计更新等)
- 性能平衡:如果太频繁(如1ms),会增加CPU开销;如果太慢(如10s),状态更新不及时
- 历史经验:经过大量测试验证,100ms是一个性能和及时性的最佳平衡点
serverCron的主要任务:
- 每100ms:更新LRU时钟、检查过期键、统计更新
- 每5秒:检查内存使用、持久化状态
- 每1分钟:执行内存碎片整理
- 每1小时:检查备份状态
3. 当客户端发送一个非常大的命令(1GB)时,Redis会如何处理?
Redis的处理流程:
- 内存分配:Redis会尝试分配1GB的内存来存储这个命令
- 内存检查:如果
maxmemory限制被超过,Redis会根据淘汰策略处理 - 阻塞风险:大命令会阻塞整个服务器,因为Redis是单线程的
可能的结果:
- 如果有足够内存:分配成功,但会导致其他客户端等待
- 如果内存不足:触发OOM错误或淘汰key
- 如果
timeout设置:客户端超时断开
建议:
- 设置
client-query-buffer-limit限制客户端输入缓冲区大小 - 使用
timeout设置客户端超时时间 - 避免执行大key操作(如
KEYS *、大hash的HGETALL)
4. epoll的ET边缘触发模式对比LT水平触发模式,对Redis有什么影响?
ET(边缘触发)vs LT(水平触发):
| 特性 | ET(边缘触发) | LT(水平触发) |
|---|---|---|
| 触发条件 | 状态变化时触发 | 只要条件满足就触发 |
| 事件丢失 | 可能丢失(需一次性读完) | 不会丢失 |
| 系统调用 | 更少(减少epoll_wait返回) | 更多 |
| 编程复杂度 | 高(需处理EAGAIN) | 低 |
Redis使用ET模式:
- 减少epoll_wait返回次数,提高性能
- 必须一次性读取所有数据(直到EAGAIN),否则可能丢失事件
- Redis的readQueryFromClient函数会循环读取直到EAGAIN
代码实现:
// ae.c
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
events = epoll_wait(eventLoop->apidata.epfd,
eventLoop->apidata.events,
eventLoop->apidata.setsize,
timeout);
// ET模式:必须一次性处理所有事件
for (j = 0; j < events; j++) {
// 处理事件
}
}
建议:ET模式性能更好,但需要更小心地处理事件。Redis已经正确实现了ET模式。

浙公网安备 33010602011771号