剥洋葱式推演:一步步彻底搞懂 Redis 的 I/O 多路复用
我们要回答的核心问题是:为什么 Redis 只有一个主线程,却能同时服务几万个客户端,且不被卡死?
为了搞懂这个,我们必须回到一切的起点——最原始的网络通信。
第一步:认清物理现实(网络请求分为“等”和“做”)
假设我们写了一段最基础的网络服务器代码,只用一个单线程。当一个客户端(Client A)连接上来时,服务端要处理它的请求,必然要经历两个阶段:
等数据(Wait for data):数据从客户端通过网络电缆,慢慢悠悠地传到网卡,再进入操作系统的缓冲区。
做事情(Copy and Execute):数据到齐了,主线程把数据读进内存,执行命令(比如
SET key value)。
痛点出现了:阶段 2 的执行速度在内存中是极快(微秒级)的;但是阶段 1 的“等”是受网络波动、用户输入速度影响的,可能是几秒甚至几分钟。 如果单线程在处理 Client A 时,Client A 一直不发数据,这个单线程就会一直卡在阶段 1(被阻塞休眠),此时哪怕 Client B 带着数据来了,单线程也无法去接待 Client B。这就是传统的同步阻塞 I/O (BIO)。
第二步:常规解法与死胡同(多线程的代价)
单线程会被一个慢客户端卡死,那最直觉的解决办法是什么?多线程。
来一个客户端,我就为它new一个专属的线程。
Client A 来,分配线程 1。线程 1 去慢慢等数据。
Client B 来,分配线程 2。线程 2 立刻处理。
看似完美,但为什么 Redis 不用?因为 Redis 的目标是同时处理 10 万个连接。如果开 10 万个线程:
内存爆炸:每个线程本身就要消耗内存。
CPU 瘫痪(上下文切换):CPU 核心数有限,要在 10 万个线程之间来回切换,光是“切换”的动作就把 CPU 算力耗尽了,根本没时间执行真正的命令。
锁竞争:这么多线程同时修改内存里的数据,为了保证安全必须加锁,一加锁性能又暴跌。
结论:多线程是一条死胡同。Redis 决定死守“单线程”,避免锁和上下文切换。那么,单线程怎么解决被卡死的问题呢?
第三步:思维破局(非阻塞与多路复用)
既然单线程不能在某一个客户端上“傻等”,那就必须改变规则。
生活中的比喻:老师(单线程)收 30 个学生(客户端)的试卷。
传统模式:老师站在学生 1 桌前,看着他写,写完收卷,再去学生 2 桌前。如果学生 1 写了半小时,老师就傻站半小时。(这就是刚才讲的 BIO)。
无脑轮询模式(NIO /
select/poll):老师不停地在教室里转圈:“你写完没?你写完没?”转了 100 圈,发现只有 1 个学生写完了。老师累得半死(CPU 空转占用极高)。多路复用模式(I/O Multiplexing):老师坐在讲台上喝茶。规定:谁写完了,就把手举起来(触发事件)。老师只要一看有人举手,就直接走过去收卷。
在这里:
多路= 多个网络连接(30 个学生)
复用= 复用同一个主线程(1 个老师) 这就是 I/O 多路复用的本质核心:用一个线程,集中监听多个连接的“就绪状态”。有数据来的,我才去处理;没数据来的,我绝不在你身上浪费时间。
第四步:底层的杀手锏(操作系统的epoll)
上面比喻中的“多路复用”,在 Linux 操作系统底层,就是大名鼎鼎的epoll函数。
单线程的 Redis 把所有连接进来的客户端 Socket,全部交给了操作系统的epoll去管理。
红黑树监听:
epoll在内核里维护了一棵红黑树,里面挂着所有连接的客户端。事件驱动机制(绝杀技):当某一个客户端的网络数据到达网卡时,网卡会通过硬件中断告诉 CPU。操作系统底层会把这个处于活跃状态的连接,立刻放进一个名叫“就绪链表”的队列里。
精准打击:Redis 主线程只需每次去问
epoll:“就绪链表里有东西吗?”有的话直接拿出来处理。时间复杂度从轮询的 O(N) 变成了 O(1)。
第五步:Redis 的最终拼图(Reactor 事件模型)
有了epoll作为底座,Redis 在自己代码里设计了一套完整的流水线,这就是Reactor 模式(文件事件处理器)。
整个流水线如下:
大管家(多路复用程序):死死盯住
epoll返回的就绪链表。事件队列:大管家把那些发来数据、准备好的连接,排成一条长队(事件队列)。
单线程执行器(文件事件分派器):Redis 那个传说中的单线程,以极快的速度从队列中拿出一个个连接,执行对应的命令(因为是纯内存操作,这步极快),写回结果。然后再拿下一个。
总结串联
让我们把完整的逻辑链串起来: 为什么 Redis 单线程能抗住高并发? 因为它绝不把时间浪费在“等待网络传输”上。 它通过I/O 多路复用(epoll),让操作系统充当大管家,代替它监控成千上万的连接。 只有当数据真的到了网卡、准备就绪时,才会排成队列交给单线程处理。 单线程省去了所有的上下文切换和锁竞争,以纯内存的速度执行完毕,从而实现了看似违背常理的极致性能。
