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