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多线程模型的工作流程,也是目前生产环境最常用的方案。

主从Reactor多线程模型流程图 主从Reactor多线程模型流程图 客户端连接 主Reactor (Boss) 只处理accept 从Reactor组 (Worker) 处理read/write事件 通常N个 = CPU核心数 Worker 1 Worker 2 Worker N 业务线程池 处理解码、业务逻辑 编码、写出 回写响应 注册Channel 流程说明: 1. 客户端发起连接,主Reactor的Selector收到accept事件 2. 主Reactor接收连接,将SocketChannel注册到某个从Reactor 3. 从Reactor监听该Channel的read/write事件 4. 数据到达,从Reactor读取后提交给业务线程池 5. 业务线程池处理完,通过从Reactor写出响应 6. 主从Reactor各自独立运行,互不阻塞

2.6 避坑指南

我这些年用Reactor模型踩过几个坑,分享给你:

  • 单Reactor单线程里做阻塞操作:我曾经在Handler里直接调了一个慢速数据库查询,结果所有连接排队等,超时雪崩。解决方案就是换成多线程模型,或者用异步回调。
  • 从Reactor数量设置不当:有人觉得越多越好,设了64个。结果线程上下文切换开销比处理事件还大。我建议从Reactor数量等于CPU核心数,最多不超过2倍。
  • 主Reactor也处理业务:这个错误比较隐蔽。主Reactor只做accept,千万别在里面做任何耗时操作。我见过有人把SSL握手放在主Reactor里,结果新连接建立慢得像蜗牛。

选型建议:如果你在写一个轻量级工具,连接数不多,单Reactor单线程就够了。如果是生产环境的高并发服务,直接上主从Reactor多线程模型,别犹豫。Netty、Redis 6.0+、Nginx都是这么干的。