2、Reactor模式详解:单Reactor单线程、单Reactor多线程、主从Reactor多线程
Reactor模式,说白了就是网络编程里的「事件分发器」。它把网络请求的接收、处理、分发拆开,让一个线程或一组线程专心等着事件来,来了就喊对应的人去干活。
我刚开始接触Netty源码时,看到Reactor这三个变种也懵了一阵。后来在项目里踩过坑,才真正理解它们各自的适用场景。今天咱们就把这三种模式掰开揉碎讲清楚。
核心思想:Reactor负责监听和分发事件(accept、read、write),Handler负责处理业务逻辑。分工明确,各司其职。
2.1 单Reactor单线程模型
这是最基础的版本。一个线程干所有事:监听连接、接收数据、处理业务、发送响应。全部串行执行。
// 伪代码示意
class SingleReactor {
public void run() {
while (true) {
selector.select(); // 阻塞等待事件
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
accept(key); // 接收连接
} else if (key.isReadable()) {
readAndProcess(key); // 读取并处理业务
} else if (key.isWritable()) {
write(key); // 写出响应
}
}
}
}
}
优点:简单、没有并发问题、调试方便。我在早期做一个小型监控系统时就用过,QPS几百,跑得很稳。
缺点:一个线程处理所有,业务逻辑稍微慢一点,整个系统就卡住了。CPU多核优势完全浪费。
| 维度 | 说明 |
|---|---|
| 适用场景 | 连接数少(几百以内)、业务处理快(纯内存计算) |
| 瓶颈 | 业务逻辑阻塞 → 所有连接都等 |
| 典型代表 | Redis 6.0之前的单线程模型(但Redis业务极快) |
注意:千万别在业务处理里做耗时操作,比如查数据库、调远程API。我曾经见过有人在这个模型里写了个sleep(1000),整个服务直接瘫痪。
2.2 单Reactor多线程模型
Reactor线程还是只有一个,只负责监听和分发事件。业务处理扔给线程池。这样Reactor线程不会被阻塞。
class ReactorWithWorkerPool {
private ExecutorService workerPool = Executors.newFixedThreadPool(4);
public void run() {
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
accept(key);
} else if (key.isReadable()) {
// 读取数据后,提交到线程池处理
workerPool.submit(() -> processBusiness(key));
}
}
}
}
}
改进点:业务处理不再阻塞Reactor线程。Reactor只做IO事件分发,吞吐量明显提升。
遗留问题:Reactor线程本身还是单点。如果连接数上万,accept和read/write操作本身也会耗时。Reactor线程忙不过来,事件处理就会延迟。
我记得有一次压测,连接数到5000时,单Reactor线程的CPU已经100%了,但worker线程池才用了30%。问题不在业务处理,在事件分发本身成了瓶颈。
2.3 主从Reactor多线程模型
这是目前最主流的方案。把Reactor拆成两组:
- 主Reactor(Boss):只负责accept新连接
- 从Reactor(Worker):负责已连接Socket的读写事件
每个从Reactor都有自己的Selector,可以独立处理读写事件。多个从Reactor可以充分利用多核CPU。
class MainSubReactor {
private Reactor bossReactor; // 主Reactor,1个
private Reactor[] workers; // 从Reactor,多个
public void run() {
// boss接收连接,注册到worker
bossReactor.setAcceptHandler((channel) -> {
Reactor worker = getNextWorker(); // 轮询选择
worker.registerChannel(channel);
});
bossReactor.run();
// 每个worker独立运行
for (Reactor worker : workers) {
worker.run();
}
}
}
为什么好?
- accept和读写分离,互不干扰
- 多个从Reactor并行处理读写,CPU利用率高
- 每个从Reactor的事件队列更短,延迟更低
实践建议:主Reactor通常1个就够了,从Reactor数量建议等于CPU核心数。Netty默认就是这种模型,BossGroup和WorkerGroup。
2.4 三种模型对比
| 模型 | Reactor线程数 | 业务处理 | 连接数 | CPU利用率 | 复杂度 |
|---|---|---|---|---|---|
| 单Reactor单线程 | 1 | Reactor线程内 | 低(≤500) | 低 | 低 |
| 单Reactor多线程 | 1 | 线程池 | 中(≤5000) | 中 | 中 |
| 主从Reactor多线程 | 1+N | 线程池 | 高(万级以上) | 高 | 较高 |
2.5 核心流程图
下面这张图展示了主从Reactor多线程模型的工作流程,也是目前生产环境最常用的方案。
2.6 避坑指南
我这些年用Reactor模型踩过几个坑,分享给你:
- 单Reactor单线程里做阻塞操作:我曾经在Handler里直接调了一个慢速数据库查询,结果所有连接排队等,超时雪崩。解决方案就是换成多线程模型,或者用异步回调。
- 从Reactor数量设置不当:有人觉得越多越好,设了64个。结果线程上下文切换开销比处理事件还大。我建议从Reactor数量等于CPU核心数,最多不超过2倍。
- 主Reactor也处理业务:这个错误比较隐蔽。主Reactor只做accept,千万别在里面做任何耗时操作。我见过有人把SSL握手放在主Reactor里,结果新连接建立慢得像蜗牛。
选型建议:如果你在写一个轻量级工具,连接数不多,单Reactor单线程就够了。如果是生产环境的高并发服务,直接上主从Reactor多线程模型,别犹豫。Netty、Redis 6.0+、Nginx都是这么干的。