2. 响应式基础与Reactor核心概念

Spring Cloud Gateway 建立在 Spring WebFlux 之上,而 WebFlux 的底层编程模型是响应式流(Reactive Streams)。要真正理解 Gateway 的过滤器链、限流、超时和线程模型,必须先掌握 Reactor 的核心概念:Mono、Flux、背压、调度器以及操作符组合。本节从“是什么”讲到“怎么用”,并解释为什么 Gateway 选择 WebFlux 而不是 Spring MVC。

一、从阻塞式到响应式:编程模型的根本差异

传统 Spring MVC 基于 Servlet 容器,采用“一请求一线程”的同步阻塞模型。当 Controller 调用数据库或远程 HTTP 接口时,当前线程会被挂起等待结果返回。假设 Tomcat 最大线程数为 200,每个请求平均阻塞 50ms,那么理论吞吐量约为 200 / 0.05 = 4000 QPS;一旦下游变慢到 500ms,吞吐量会骤降到 400 QPS,线程池迅速被占满,新请求只能排队甚至被拒绝。

响应式编程则把“等待”变成“回调 + 事件驱动”。调用方不再阻塞线程,而是返回一个“未来会产出数据”的发布者(Publisher),由事件循环在数据就绪时触发后续处理。少量线程即可支撑大量并发连接,这正是网关这种高并发、IO 密集型场景所需要的。

需要强调:响应式并不等于更快。对于 CPU 密集型任务,它没有优势;它的价值在于用更少的线程资源承载更高的并发连接数,并让背压成为一等公民。

二、Mono 与 Flux:Reactor 的两个核心类型

Reactor 实现了 Reactive Streams 规范,最常用的两个发布者是:

  • Mono<T>:表示 0 或 1 个元素的异步序列,适合“单个结果”的场景,如一次查询返回一个对象、一次鉴权返回通过/拒绝。
  • Flux<T>:表示 0 到 N 个元素的异步序列,适合“流式结果”,如 SSE 推送、批量数据、日志流。

两者都是惰性的:声明时不会执行任何逻辑,只有订阅(subscribe)后才真正触发。下面是一个可直接运行的示例,演示 Mono 与 Flux 的创建与消费:

// Mono:0 或 1 个元素
Mono<String> mono = Mono.just("gateway")
        .map(String::toUpperCase)
        .delayElement(Duration.ofMillis(100));

// Flux:0 到 N 个元素
Flux<Integer> flux = Flux.range(1, 5)
        .filter(i -> i % 2 == 1)
        .map(i -> i * 10);

mono.subscribe(v -> System.out.println("mono=" + v));
flux.subscribe(v -> System.out.println("flux=" + v));
// 输出:flux=10 flux=30 flux=50,随后 mono=GATEWAY

注意输出顺序:Flux 是同步立即发射的,而 Mono 因为 delayElement 延迟了 100ms,所以后打印。这说明响应式序列的完成时机由调度器和操作符共同决定,而不是代码书写顺序。

三、背压机制:让消费者决定节奏

背压(Backpressure)是响应式流区别于普通回调的关键。当生产者速度远快于消费者时,如果没有背压,数据会堆积在内存中导致 OOM。Reactive Streams 通过 request(n) 协议让消费者主动声明“我还能处理 n 个”,生产者据此控制发送速率。

Reactor 中常见的背压策略包括:

  • BUFFER:默认策略,缓存所有未能及时处理的数据,风险是内存增长。
  • DROP:队列满时丢弃新数据,适合可容忍丢失的监控指标。
  • LATEST:只保留最新一条,适合实时状态展示。
  • ERROR:队列满时抛出异常,适合必须严格处理的场景。

在 Gateway 中,背压直接影响响应体写回:如果下游服务返回大文件而客户端读取缓慢,WebFlux 会通过背压暂停从下游读取,避免网关内存被撑爆。

四、调度器:控制任务在哪个线程执行

Reactor 通过 Scheduler 抽象线程调度,常用类型如下:

调度器线程模型典型用途
Schedulers.immediate()当前线程测试、同步执行
Schedulers.single()单线程复用低并发顺序任务
Schedulers.parallel()固定大小线程池(CPU 核数)CPU 密集型计算
Schedulers.boundedElastic()弹性线程池,有上限包装阻塞式调用

关键操作符是 publishOn 和 subscribeOn:publishOn 影响其后操作符的执行线程,subscribeOn 影响订阅发生的位置。在 Gateway 中,Netty 事件循环线程负责网络 IO,绝不能在其中执行阻塞调用,否则会拖垮整个事件循环。若必须调用阻塞接口,应使用 subscribeOn(Schedulers.boundedElastic()) 将其隔离。

五、操作符组合:把异步逻辑串起来

Reactor 提供大量操作符,常用的组合方式包括:

  • 转换:map、flatMap、concatMap。
  • 过滤:filter、take、skip。
  • 合并:merge、zip、concat。
  • 错误处理:onErrorResume、onErrorReturn、retry。
  • 超时:timeout、timeout(Duration)。

下面示例演示在网关过滤器中常见的“鉴权 + 转发 + 超时兜底”组合:

Mono<Void> chain = authService.check(token)
    .flatMap(ok -> chain.filter(exchange))
    .timeout(Duration.ofSeconds(3))
    .onErrorResume(TimeoutException.class, e -> {
        exchange.getResponse().setStatusCode(HttpStatus.GATEWAY_TIMEOUT);
        return exchange.getResponse().setComplete();
    });

这段代码体现了响应式的核心优势:鉴权、转发、超时、兜底被组合成一条声明式流水线,没有阻塞、没有回调地狱,且超时和错误处理是显式声明的。

六、为什么 Gateway 基于 WebFlux 而非 Spring MVC

Spring Cloud Gateway 的定位是 API 网关,需要同时处理成千上万的连接,并频繁与下游服务、限流组件、认证服务交互。这些操作几乎都是 IO 密集型,而不是 CPU 密集型。如果基于 Spring MVC,每个请求占用一个线程,在高并发下线程池会成为瓶颈,且无法优雅地支持 SSE、WebSocket 等流式协议。

WebFlux 基于 Netty 和 Reactor,用少量事件循环线程处理大量连接,天然支持背压和流式传输,正好匹配网关的需求。此外,Gateway 的过滤器链本身就是用 Mono/Flux 编排的,限流、重试、熔断等能力都可以通过操作符组合实现,扩展性更强。

记忆要点:Mono 是 0/1,Flux 是 0/N;背压是消费者驱动的 request(n) 协议;publishOn 管后续线程,subscribeOn 管订阅线程;阻塞调用必须隔离到 boundedElastic;Gateway 选 WebFlux 是为了高并发 IO 和流式能力。
Reactor 响应式数据流与背压示意 生产者 Publisher Mono / Flux 惰性,订阅后触发 操作符组合链 map / filter / flatMap / zip timeout / retry / onErrorResume publishOn / subscribeOn 消费者 Subscriber subscribe 触发 onNext / onError / onComplete 背压 request(n):消费者声明可处理数量,生产者据此限速 调度器 Scheduler:决定任务在哪个线程执行 parallel() CPU 密集 boundedElastic() 阻塞隔离 single() 单线程 immediate() 当前线程 Spring MVC:一请求一线程,阻塞等待 线程池易耗尽,流式支持弱 WebFlux + Reactor:事件驱动,非阻塞 少量线程高并发,天然背压与流式

把上面的图记在脑子里,再去看 Gateway 的 GlobalFilter 和 GatewayFilter,你会发现它们返回的都是 Mono<Void>,过滤器链就是一条由操作符串起来的响应式流水线。理解了 Mono、Flux、背压和调度器,后续学习路由断言、过滤器顺序、限流熔断时,就不会停留在“配置怎么写”,而是能判断“为什么这样写、线程会怎么走、异常会怎么传播”。