第四章: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模型,也是我最早接触的版本。

说白了就是:一个线程干所有事——监听事件、接收连接、处理读写、执行业务逻辑,全在一个线程里完成。

它的结构图如下:

单Reactor单线程模型 Reactor(单线程) Selector Acceptor Handler Handler 客户端1 客户端2 客户端3

这个模型的优点很明显:简单、没有并发问题。所有操作都在一个线程里,不需要考虑锁、不需要考虑线程安全。

但缺点也致命:

  • 如果某个Handler处理慢了,所有连接都得等着
  • 多核CPU只能用一个核,资源浪费严重
  • 一旦线程阻塞,整个服务就挂了

避坑指南:我曾经在一个小型的IoT网关项目里用了这个模型,设备量只有200多台,跑得挺欢。后来客户要求扩展到2000台,结果CPU直接飙到100%,响应时间从5ms变成了500ms。这就是单线程模型的瓶颈——它扛不住计算密集型的业务。

4.3 单Reactor多线程模型

既然单线程扛不住,那就加线程呗。

单Reactor多线程模型的核心思路是:Reactor线程只负责I/O事件的分发,业务逻辑交给工作线程池去处理。

我习惯把这个模型叫做「分工协作模式」。Reactor就像个前台接待,只负责登记来访者(I/O事件),然后让后台的工作人员(工作线程)去处理具体事务。

它的工作流程:

  1. Reactor线程通过Selector监听事件
  2. 收到读事件后,读取数据
  3. 将读取到的数据封装成任务,丢给工作线程池
  4. Reactor线程继续监听下一个事件
  5. 工作线程处理完业务后,将结果写回

代码示意:

// 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模型的结构:

主从Reactor多线程模型 主Reactor(1个) 主Selector Acceptor 从Reactor组(通常为CPU核心数) 从Reactor1 从Reactor2 从Reactor3 从ReactorN Handler(每个从Reactor绑定一组Handler) 工作线程池(处理业务逻辑)

这个模型的好处:

  • 连接处理与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模式的三种模型就讲到这里。从单线程到主从多线程,本质上是在「简单」和「性能」之间做权衡。没有银弹,只有最适合你场景的方案。