Redis网络模型-非阻塞IO
一、前言:从“等待”到“询问”的进化
在上一篇文章中,我们探讨了阻塞 IO(BIO) 的局限性——一个线程只能服务一个连接,导致在高并发下资源耗尽。那么,有没有一种方式,能让一个线程同时处理多个连接,而又不被任何一个连接的慢速操作所“卡住”呢?
答案就是非阻塞 IO(Non-blocking I/O, NIO)。
非阻塞 IO 是对阻塞 IO 的一次重要改进,它改变了进程与内核交互的方式。虽然 Redis 并未直接使用原始的非阻塞 IO 模型(因为它有更优的解决方案),但理解 NIO 是通往 Redis 核心网络模型——IO 多路复用——的关键桥梁。
💡 核心价值:
非阻塞 IO 的核心思想是“不等待”,它让线程可以主动去“询问”数据是否就绪,从而避免了在单个连接上无谓的阻塞。然而,单纯的“轮询”又带来了新的 CPU 开销问题,这正是 IO 多路复用要解决的!
本文将带你:
- 彻底搞懂非阻塞 IO 的工作原理
- 剖析其“轮询”模式带来的新问题
- 理解 Redis 如何将其与 IO 多路复用结合,发挥最大效能
二、什么是非阻塞 IO?一个忙碌的餐厅经理
继续用餐厅的比喻来理解。
现在,餐厅里有一位非常勤快但不懂变通的经理(用户进程)。
- 他走到 1 号桌客人(socket 1)面前,问:“您的牛排好了吗?”(发起
read()调用)。 - 厨房(内核)回答:“还没好。”
- 经理不会傻等!他立刻转身,马上走到 2 号桌客人(socket 2)面前,问同样的问题。
- 如果 2 号桌也没好,他就去问 3 号桌、4 号桌……如此循环往复。
- 直到某次他问到 N 号桌时,厨房说:“好了!” 他才把牛排端给客人,然后继续他的“巡逻”。
在这个场景中,经理(线程)永远不会停下来等待,他一直在主动“询问”(轮询)。这就是非阻塞 IO 的本质。
技术定义
在 Linux 中,可以通过 fcntl(fd, F_SETFL, O_NONBLOCK) 将一个文件描述符(如 socket)设置为非阻塞模式。
- 当对该 fd 执行
read()时:- 如果数据已准备好,内核会正常拷贝数据并返回读取的字节数。
- 如果数据未准备好,内核不会阻塞进程,而是立即返回一个错误
EAGAIN或EWOULDBLOCK。
- 进程收到这个错误后,就知道“现在没数据”,可以去做别的事,或者稍后再来尝试。
✅ 关键特性:同步 + 非阻塞。进程不会在 IO 调用上挂起,但它必须自己负责不断地检查 IO 是否就绪。
三、非阻塞 IO 的工作流程
让我们看看代码层面是如何工作的:
// 1. 将 socket 设置为非阻塞模式
int flags = fcntl(client_fd, F_GETFL, 0);
fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
while (1) {
// 2. 尝试读取数据
ssize_t n = read(client_fd, buffer, sizeof(buffer));
if (n > 0) {
// 成功读取到数据,处理它
process(buffer, n);
} else if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 3. 数据未就绪!这是非阻塞IO的核心
// 此时,线程不能在这里等待,必须做点别的
// ... 例如,去检查其他 socket ...
} else {
// 其他错误,如连接断开
handle_error();
}
}
}
⚠️ 核心挑战:第 3 步。当
read返回EAGAIN时,线程接下来该做什么?如果它只是在一个 tight loop 里不断重试同一个read,那和阻塞没区别,甚至更糟(因为占满了 CPU)。所以,它必须去轮询其他所有需要监听的 socket。
四、非阻塞 IO 的致命缺陷:CPU 空转
非阻塞 IO 解决了“线程被单个连接阻塞”的问题,但它引入了一个新的、同样严重的问题:CPU 空转(Busy Waiting)。
问题场景
假设服务器有 10,000 个客户端连接,但其中 9,999 个都处于空闲状态,只有 1 个连接偶尔有数据。
- 采用非阻塞 IO 的服务器,其主线程必须在一个循环里,依次对这 10,000 个 socket 调用
read。 - 对于那 9,999 个空闲连接,每次
read都会立刻返回EAGAIN。 - 结果就是,CPU 的绝大部分时间都花在了执行这 9,999 次无意义的系统调用上,而真正处理业务逻辑的时间微乎其微。
结论:单纯的非阻塞 IO 模型,虽然解决了线程阻塞问题,却以极高的 CPU 占用率为代价,在高并发、低活跃度的场景下效率极低。
五、Redis 的智慧:非阻塞 IO + IO 多路复用
Redis 并没有止步于非阻塞 IO,而是将它与更强大的 IO 多路复用(I/O Multiplexing) 技术结合起来,创造出了近乎完美的解决方案。
5.1 为什么需要两者结合?
- IO 多路复用(如
epoll) 的作用是:告诉你哪些 socket 已经准备好了(可读/可写)。 - 非阻塞 IO 的作用是:当你去处理这些“已就绪”的 socket 时,确保你的
read/write调用不会意外地再次阻塞。
5.2 Redis 的工作流程(精简版)
- 初始化:Redis 启动时,会将所有客户端 socket 都设置为非阻塞模式。
- 注册监听:将这些 socket 注册到
epoll实例中,并声明关心EPOLLIN(可读)事件。 - 主循环:
while (1) { // (1) 阻塞在这里,直到有 socket 就绪 int num_ready = epoll_wait(epfd, events, max_events, timeout); // (2) epoll_wait 返回后,events 数组里包含了所有就绪的 socket for (int i = 0; i < num_ready; i++) { int fd = events[i].data.fd; // (3) 因为 socket 是非阻塞的,这里的 read 不会阻塞 // 即使一次没读完(TCP是流式协议),下次 epoll 会再次通知 handle_client_request(fd); // 内部调用 read/write } } - 关键点:
epoll_wait替我们完成了高效的“轮询”工作,并且是由内核在硬件中断驱动下完成的,效率极高。- 当我们处理
epoll_wait返回的就绪 socket 时,由于它们已经是非阻塞的,read调用可以安全地一次性读取所有可用数据,而不用担心被卡住。
✅ 优势:
- 零无效轮询:线程只在
epoll_wait上阻塞,不会浪费 CPU 去检查未就绪的 socket。- 绝对不阻塞:即使某个连接的数据量很大,分多次才能读完,由于 socket 是非阻塞的,每次
read都能立即返回,保证了主线程的流畅性。
5.3 总结关系
| 组件 | 角色 |
|---|---|
IO 多路复用 (epoll) |
侦察兵:高效地监控成千上万个连接,并精准报告哪些连接有事发生。 |
| 非阻塞 IO | 执行者:确保在处理侦察兵报告的“有事”连接时,动作干净利落,绝不拖泥带水。 |
二者缺一不可,共同构成了 Redis 单线程高并发的基石。
六、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)