引言
Redis单线程却能处理10000+并发连接,秘密就在于IO多路复用(I/O Multiplexing)。今天我们深入理解这个高并发的核心机制。
一、什么是IO多路复用?
核心思想:
一个线程监听多个文件描述符(socket连接),哪个准备好就处理哪个
传统阻塞I/O:
线程1 → 监听客户端1 → 阻塞等待
线程2 → 监听客户端2 → 阻塞等待
...
线程N → 监听客户端N → 阻塞等待
IO多路复用:
线程1 → 监听所有客户端 → 有事件就处理,没事件就等待
二、三种实现:select、poll、epoll
2.1 select(最古老,1983年)
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
select(max_fd + 1, &readfds, NULL, NULL, NULL);
// 缺点:
// 1. fd数量限制(默认1024)
// 2. 需要遍历所有fd检查哪个就绪(O(n))
// 3. 每次调用需要拷贝fd_set到内核
2.2 poll(1997年)
struct pollfd fds[N];
fds[0].fd = fd1;
fds[0].events = POLLIN;
poll(fds, N, -1);
// 改进:无fd数量限制
// 缺点:仍需遍历所有fd(O(n))
2.3 epoll(Linux 2.6,2002年)
// 1. 创建epoll实例
int epfd = epoll_create(1);
// 2. 添加监听的fd
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd1;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev);
// 3. 等待事件
struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
// 优势:
// ✅ O(1)时间复杂度(只返回就绪的fd)
// ✅ 无fd数量限制
// ✅ 无需每次拷贝fd列表
性能对比:
| 特性 | select | poll | epoll |
|---|---|---|---|
| fd数量限制 | ❌ 1024 | ✅ 无限制 | ✅ 无限制 |
| 时间复杂度 | O(n) | O(n) | O(1) |
| 内核拷贝 | 每次 | 每次 | 无需 |
| 适用连接数 | < 1000 | < 5000 | > 10000 |
Redis的选择:
- Linux: epoll
- macOS/FreeBSD: kqueue
- Windows: select(Windows性能差)
三、Redis的Reactor模式
3.1 事件循环(Event Loop)
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 1. 执行beforesleep回调
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 2. 等待事件(epoll_wait)
numevents = aeApiPoll(eventLoop, tvp);
// 3. 处理文件事件
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[fd];
// 可读事件(接收客户端命令)
if (fe->mask & AE_READABLE) {
fe->rfileProc(eventLoop, fd, fe->clientData, mask);
}
// 可写事件(发送响应)
if (fe->mask & AE_WRITABLE) {
fe->wfileProc(eventLoop, fd, fe->clientData, mask);
}
}
// 4. 处理时间事件(定时任务)
processTimeEvents(eventLoop);
}
}
3.2 文件事件处理
客户端发送命令:
1. epoll_wait返回fd就绪
2. 调用readQueryFromClient(读取命令)
3. 解析命令(RESP协议)
4. 执行命令(单线程处理)
5. 将响应加入输出缓冲区
6. 注册可写事件
7. epoll_wait返回可写
8. 调用sendReplyToClient(发送响应)
3.3 时间事件处理
// 定时任务
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// 1. 更新统计信息
updateCachedTime();
// 2. 检查客户端超时
clientsCron();
// 3. 触发BGSAVE、AOF重写
backgroundSaveDoneHandler();
// 4. 渐进式rehash
if (server.activerehashing) {
dictRehashMilliseconds(server.db[j].dict, 1);
}
return 100; // 100ms后再次执行
}
四、epoll的两种触发模式
4.1 水平触发(LT,Level Triggered)
特点:只要fd就绪,epoll_wait就会返回
示例:
1. 客户端发送100字节
2. epoll_wait返回(可读)
3. 读取50字节
4. epoll_wait再次返回(仍可读,剩余50字节)
优势:不会丢失事件
劣势:可能重复通知
Redis使用LT模式(更简单、可靠)
4.2 边缘触发(ET,Edge Triggered)
特点:只在fd状态变化时通知一次
示例:
1. 客户端发送100字节
2. epoll_wait返回(可读)
3. 读取50字节
4. epoll_wait不再返回(需要一次性读完)
优势:减少通知次数
劣势:容易丢失事件(需要非阻塞+循环读取)
五、性能优化技巧
5.1 Pipeline批量操作
// 减少网络往返
redis.executePipelined((RedisCallback<Object>) connection -> {
for (int i = 0; i < 10000; i++) {
connection.set(("key:" + i).getBytes(), ("value:" + i).getBytes());
}
return null;
});
// 效果:
// - 10000次操作 → 1次网络往返
// - epoll_wait调用次数:10000 → 2(1次读,1次写)
5.2 避免慢命令
# ❌ 不好:阻塞事件循环
KEYS * # O(n)
FLUSHALL # O(n)
# ✅ 好:使用渐进式命令
SCAN 0 COUNT 100 # 分批获取
FLUSHALL ASYNC # 异步删除
六、总结
核心要点
IO多路复用
- 一个线程监听多个连接
- epoll:O(1)时间复杂度,支持海量连接
Reactor模式
- 事件循环:等待事件 → 处理事件
- 文件事件:客户端请求/响应
- 时间事件:定时任务
epoll优势
- 无fd数量限制
- O(1)获取就绪fd
- 无需每次拷贝fd列表
LT vs ET
- Redis使用LT模式
- 简单可靠,不会丢失事件
下一篇预告
理解了事件驱动架构后,你是否好奇:Redis主从复制如何实现数据同步?全量复制和增量复制有什么区别?
下一篇《主从复制:数据同步机制》,我们将深入剖析Redis的主从架构。
思考题:
- 为什么epoll比select/poll快?
- Redis为什么选择LT模式而不是ET模式?
- 如果10000个客户端同时发送命令,Redis如何保证公平性?