第四章:Reactor模式:从单线程到主从多线程的演进之路
Reactor模式,说白了就是网络编程里的「事件分发器」。
我刚开始接触网络编程时,总觉得这玩意儿有点玄乎。后来在做一个高并发网关项目时,才真正体会到它的威力。当时系统动不动就卡死,排查下来发现是传统的BIO模型在作祟——每个连接一个线程,连接一多,线程数暴涨,CPU全耗在上下文切换上了。
嗯,Reactor模式就是来解决这个问题的。
4.1 Reactor模式的核心原理
Reactor模式的核心思想其实很简单:把I/O事件的处理权交给一个中央调度器。
你想想看,传统的做法是每个连接自己盯着socket,看有没有数据来。Reactor的做法是:大家都别盯着了,我来统一监听,谁有数据来了我通知谁。
这个模式包含三个关键角色:
- Reactor:事件循环的核心,负责监听和分发事件
- Handler:事件处理器,负责处理具体的业务逻辑
- Acceptor:连接接收器,专门处理新连接事件
用伪代码来描述Reactor的工作流程:
while (true) {
events = selector.select(); // 阻塞等待事件
for (event : events) {
if (event.isAcceptable()) {
acceptor.handle(); // 处理新连接
} else if (event.isReadable()) {
handler.read(); // 处理读事件
} else if (event.isWritable()) {
handler.write(); // 处理写事件
}
}
}
我在项目中遇到过一个问题:刚开始用Reactor时,把业务逻辑全塞在Handler里,结果Handler处理慢了,整个事件循环都被拖住。后来才明白,Handler里只应该做非阻塞的、轻量级的操作。
核心要点:Reactor模式将I/O多路复用与事件分发相结合,实现了「一个线程管理成千上万个连接」的能力。
4.2 单Reactor单线程模型
这是最基础的Reactor模型,也是我最早接触的版本。
说白了就是:一个线程干所有事——监听事件、接收连接、处理读写、执行业务逻辑,全在一个线程里完成。
它的结构图如下:
这个模型的优点很明显:简单、没有并发问题。所有操作都在一个线程里,不需要考虑锁、不需要考虑线程安全。
但缺点也致命:
- 如果某个Handler处理慢了,所有连接都得等着
- 多核CPU只能用一个核,资源浪费严重
- 一旦线程阻塞,整个服务就挂了
避坑指南:我曾经在一个小型的IoT网关项目里用了这个模型,设备量只有200多台,跑得挺欢。后来客户要求扩展到2000台,结果CPU直接飙到100%,响应时间从5ms变成了500ms。这就是单线程模型的瓶颈——它扛不住计算密集型的业务。
4.3 单Reactor多线程模型
既然单线程扛不住,那就加线程呗。
单Reactor多线程模型的核心思路是:Reactor线程只负责I/O事件的分发,业务逻辑交给工作线程池去处理。
我习惯把这个模型叫做「分工协作模式」。Reactor就像个前台接待,只负责登记来访者(I/O事件),然后让后台的工作人员(工作线程)去处理具体事务。
它的工作流程:
- Reactor线程通过Selector监听事件
- 收到读事件后,读取数据
- 将读取到的数据封装成任务,丢给工作线程池
- Reactor线程继续监听下一个事件
- 工作线程处理完业务后,将结果写回
代码示意:
// Reactor线程
public void run() {
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isReadable()) {
// 读取数据
ByteBuffer buffer = readData(key);
// 丢给工作线程处理
threadPool.execute(() -> processBusiness(buffer, key));
}
}
}
}
// 工作线程
public void processBusiness(ByteBuffer data, SelectionKey key) {
// 处理业务逻辑(可能耗时较长)
String result = doBusiness(data);
// 写回结果
writeResponse(key, result);
}
这个模型解决了单线程的两个大问题:
- 业务处理慢不会阻塞I/O事件分发
- 可以利用多核CPU并行处理业务
但它也有自己的坑:
- Reactor线程仍然是单点——如果连接数特别大,Reactor线程本身可能成为瓶颈
- 写回操作需要同步——多个工作线程可能同时尝试写回,需要加锁
我的经验:这个模型适合「I/O轻、业务重」的场景。比如一个API网关,接收请求很快,但业务处理需要查数据库、调远程服务。这时候把业务丢给工作线程,Reactor线程就能专心处理I/O。
4.4 主从Reactor多线程模型
这是Reactor模式的终极形态,也是Netty、Redis 6.0等高性能框架采用的方案。
说白了就是:把Reactor拆成两个角色——主Reactor负责接收连接,从Reactor负责处理I/O事件。
我当年在做一个百万级长连接推送系统时,用的就是这个模型。当时单Reactor多线程模型已经扛不住了——连接数到了5万,Reactor线程的CPU就满了,因为光处理accept事件就忙不过来。
主从Reactor模型的结构:
这个模型的好处:
- 连接处理与I/O处理分离——主Reactor专心accept,从Reactor专心读写
- 天然支持多核扩展——从Reactor可以开多个,每个绑定一个CPU核心
- 减少锁竞争——每个从Reactor管理自己的连接集合,互不干扰
三种模型的对比:
| 模型 | Reactor数量 | 工作线程 | 适用场景 | 典型代表 |
|---|---|---|---|---|
| 单Reactor单线程 | 1 | 无(Reactor自己处理) | 低并发、业务简单 | Redis(单线程版) |
| 单Reactor多线程 | 1 | 有(线程池) | 中等并发、业务复杂 | 某些自定义NIO框架 |
| 主从Reactor多线程 | 1主+N从 | 有(线程池) | 高并发、高性能 | Netty、Redis 6.0 |
选型建议:我个人习惯这样选——连接数少于1000用单Reactor单线程就够了;连接数在1万以内用单Reactor多线程;超过1万或者对延迟要求极高,直接上主从Reactor多线程。
4.5 实际项目中的坑与经验
讲了这么多理论,说几个我在实际项目中踩过的坑:
坑一:Handler里做阻塞操作
我曾经在Handler里直接调了数据库查询,结果查询慢的时候,整个Reactor线程都卡住了。记住:Handler里绝对不能有阻塞操作,包括但不限于数据库查询、远程调用、文件读写、Thread.sleep()。
坑二:从Reactor数量设置不当
有次我把从Reactor数量设成了32,结果性能反而下降了。后来发现是因为线程太多,上下文切换开销超过了并行带来的收益。经验值是:从Reactor数量 = CPU核心数,最多不超过2倍。
坑三:忘记处理写事件
刚开始写Reactor时,我只关注了读事件,忽略了写事件。结果在高并发下,写缓冲区满了,数据写不出去,导致内存暴涨。后来才加上写事件的监听和背压控制。
小技巧:调试Reactor模型时,可以在每个事件处理前后打上时间戳,看看哪个环节耗时最长。我一般用这个方法来定位性能瓶颈。
好了,Reactor模式的三种模型就讲到这里。从单线程到主从多线程,本质上是在「简单」和「性能」之间做权衡。没有银弹,只有最适合你场景的方案。