Spring WebFlux 响应式编程
提出问题
传统 Spring MVC 基于 Servlet 规范,每个请求占用一个线程,直到响应返回。在高并发场景下(尤其是 IO 密集的长连接、实时数据流),线程池很快被打满,系统吞吐量受限于线程数。Tomcat 默认 200 线程,4 核 16G 的机器,压测下 500 并发就开始有线程等待和上下文切换开销。
Spring WebFlux 基于 Reactor 的 Mono/Flux 和 Netty 的非阻塞 IO,用少量线程处理大量并发请求。但 WebFlux 并非银弹——同步阻塞的数据库驱动、第三方 HTTP 调用都会拖垮它的优势。面试官问你 WebFlux,往往是在考察:你是否真正理解响应式编程的适用边界,还是仅仅把它当作「异步 MVC」来用?
分析问题
Netty 事件循环:WebFlux 的底层引擎
WebFlux 默认跑在 Netty 上,Netty 的核心是 EventLoop 模型。EventLoop 本质上是一个单线程循环,不断重复:从 Channel 读事件 → 执行 Handler → 写回响应。默认启动的 EventLoop 线程数等于 CPU 核数 × 2(比如 4 核机器起 8 个 EventLoop)。
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ EventLoop 1 │ │ EventLoop 2 │ │ EventLoop 3 │
│ (CPU 核0) │ │ (CPU 核1) │ │ (CPU 核2) │
│ select() │ │ select() │ │ select() │
│ process() │ │ process() │ │ process() │
│ runTasks() │ │ runTasks() │ │ runTasks() │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────────────────┼────────────────────┘
│
┌─────────────▼─────────────┐
│ Channel 注册到 EventLoop │
│ 每个 Channel 绑定一个 │
│ EventLoop,生命周期不变 │
└───────────────────────────┘关键约束:一个 EventLoop 负责多个 Channel,但同一个 Channel 的所有操作都在同一个 EventLoop 线程上执行,不存在锁竞争。代价是:如果你在这个 EventLoop 线程里做了一次阻塞 IO(比如 JDBC 查询),整个 EventLoop 上的所有 Channel 都得等它完成。这也是为什么 WebFlux 不能用 JDBC 的原因。
Reactor 核心:Mono 与 Flux
Reactor 是 WebFlux 的底层响应式库,提供两种核心发布者:
- Mono<T> — 表示 0 或 1 个元素的异步序列
- Flux<T> — 表示 0 到 N 个元素的异步序列
// Mono 示例:返回单个结果
Mono<String> greeting = Mono.just("Hello").map(String::toUpperCase);
// Flux 示例:返回多个结果
Flux<Integer> numbers = Flux.just(1, 2, 3, 4, 5)
.filter(n -> n > 2)
.map(n -> n * 10);关键区别:Mono/Flux 是声明式的,调用 map/filter 只是构建操作链,不执行;只有 subscribe 或 WebFlux 框架内部的订阅才会触发真正执行。这种「惰性求值」机制是构建非阻塞管道的基石。
时间线:
┌─ just("Hello") ──→ map(toUpper) ──→ subscribe ──→ 输出 "HELLO"
│ 声明阶段:只构建操作链,不执行 │
│ │
└────────── 订阅触发异步执行 ────────────────┘背压(Backpressure)
背压是响应式流规范的核心:下游可以告诉上游「我消化不了,慢点发」。Reactor 内置了多种背压策略:
| 策略 | 方法 | 行为 |
|---|---|---|
| 限流 | limitRate(n) | 上游每次最多发 n 个,不等下游请求 |
| 缓冲 | onBackpressureBuffer() | 上游元素全部缓冲到队列,可能 OOM |
| 丢弃 | onBackpressureDrop() | 上游多发的元素直接丢弃 |
| 报错 | onBackpressureError() | 背压发生时抛出异常 |
| 最新 | onBackpressureLatest() | 只保留最新元素,丢弃旧的 |
Flux.range(1, 1_000_000)
.log()
.subscribe(new BaseSubscriber<Integer>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
// 每次只请求 100 个,控制消费节奏
request(100);
}
@Override
protected void hookOnNext(Integer value) {
// 处理一个元素
if (value % 100 == 0) {
request(100); // 处理完一批,再请求下一批
}
}
});生产踩坑:我们有个实时数据推送服务,用 Flux.interval(Duration.ofMillis(10)) 每秒产生 100 个事件,下游消费端处理一个事件平均耗时 50ms。10ms 产生 vs 50ms 消费,差 5 倍。如果不加背压,未处理的事件会在 Sinks.many() 内部队列无限堆积,最终 OOM。解决办法是 limitRate(20) 让上游主动限频,配合 onBackpressureLatest() 丢弃来不及处理的旧数据,只保留最新状态。
与 Spring MVC 的核心对比
| 维度 | Spring MVC | Spring WebFlux |
|---|---|---|
| 底层 | Servlet API(阻塞 IO) | Reactor Netty / Undertow(非阻塞 IO) |
| 线程模型 | 请求 → 线程 → 响应(同线程) | 事件循环 + Work 线程(少量线程处理海量连接) |
| 默认线程数 | Tomcat 200(max 可配) | EventLoop = CPU × 2(4 核 ≈ 8 线程) |
| 编程模型 | 注解 @Controller | 注解 + 函数式 RouterFunction |
| 数据库 | JPA / JDBC(阻塞) | R2DBC / MongoDB Reactive(非阻塞) |
| 最大并发(4 核 16G) | 约 2000 TPS(Tomcat 调优后) | 约 15000+ TPS(Netty 非阻塞) |
| 内存占用(1000 连接) | 约 500MB(线程栈 × 1000) | 约 50MB(事件循环 + 少量线程) |
| 适用场景 | CPU 密集、短请求、阻塞库 | IO 密集、长连接、流式响应、网关 |
数值来源:基于 4 核 16G 机器、短请求(<50ms 处理)、纯内存操作的压测对比。实际场景中数据库 IO 会成为瓶颈,但 WebFlux 在连接数上的优势不变。
// WebFlux 函数式路由示例
@Configuration
public class RouterConfig {
@Bean
public RouterFunction<ServerResponse> route(UserHandler handler) {
return RouterFunctions.route()
.GET("/api/users/{id}", handler::getUser)
.GET("/api/users", handler::listUsers)
.POST("/api/users", handler::createUser)
.build();
}
}
@Component
class UserHandler {
private final ReactiveUserRepository repo;
public Mono<ServerResponse> getUser(ServerRequest req) {
return repo.findById(req.pathVariable("id"))
.flatMap(user -> ServerResponse.ok().bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
}
}生产实战:R2DBC 的坑
R2DBC 是响应式数据库驱动,但远没有 JDBC 成熟,以下是我们实际踩过的坑:
坑 1:连接池耗尽
// 错误写法:flatMap 无限并发,R2DBC 连接池瞬间打满
return userRepo.findAll()
.flatMap(user -> orderRepo.findByUserId(user.getId())) // 并发 N 个查询
.collectList();
// 改为:控制并发度
return userRepo.findAll()
.flatMap(user -> orderRepo.findByUserId(user.getId()), 16) // 最多 16 并发
.collectList();坑 2:事务管理 R2DBC 的 @Transactional 需要 Reactive 事务管理器,不能和 JDBC 事务管理器混用。Spring 会报 No transaction manager found。配置:
@Bean
public ReactiveTransactionManager transactionManager(DatabaseClient client) {
return new R2dbcTransactionManager(client.getConnectionFactory());
}坑 3:阻塞操作混入
// 错误:在 WebFlux 里调用阻塞 API
return userRepo.findById(id)
.map(user -> {
String encrypt = AES.encrypt(user.getEmail()); // 阻塞操作!
return user.withEmail(encrypt);
});
// 正确:切换到 Schedulers.boundedElastic() 执行阻塞代码
return userRepo.findById(id)
.publishOn(Schedulers.boundedElastic())
.map(user -> {
String encrypt = AES.encrypt(user.getEmail());
return user.withEmail(encrypt);
});不适用场景
不适用场景:
- CPU 密集计算(加解密、图像处理)—— 事件循环被阻塞,整个服务瘫痪
- 依赖 JDBC / JPA 的数据库访问——阻塞操作会挂起事件循环线程
- 团队缺乏响应式思维——调试困难、学习曲线陡峭
调试难点:WebFlux 的异常堆栈往往包含几十层 Reactor 内部操作符,难以定位到业务代码。生产实践一般加 checkpoint() 标记关键操作链,而不是全局开 Hooks.onOperatorDebug()(后者会记录全量 assembly 信息,对高频请求性能影响很大):
Flux<User> users = repo.findAll()
.checkpoint("findAllUsers", true) // 开启详细检查点,只标记关键链路
.filter(user -> user.isActive());面试常问:WebFlux 与虚拟线程的取舍
Spring Boot 3.2 + JDK 21 引入了虚拟线程(Virtual Threads),它也能用少量线程处理大量阻塞请求。那 WebFlux 还有必要吗?
| 对比维度 | WebFlux + Netty | 虚拟线程 + Tomcat |
|---|---|---|
| 阻塞操作 | 必须用 publishOn 切线程 | 随意阻塞,虚拟线程自动挂起 |
| 数据库 | 必须 R2DBC | JDBC 直接可用,阻塞时自动 yield |
| 流式响应 | 原生支持 Flux<SseEvent> | 需要 ResponseBodyEmitter,较复杂 |
| 学习成本 | 高(Mono/Flux 操作符矩阵) | 低(同步编程思维) |
| 吞吐量 | 极高(事件循环无上下文切换) | 高(虚拟线程切换开销约 1μs) |
| 成熟度 | 7 年 + | JDK 21 刚 GA,仍在迭代 |
结论:如果项目重度依赖 JDBC 或团队不熟悉响应式,优先选虚拟线程。如果要做流式 API、网关、高吞吐 IO 密集型服务,WebFlux 仍是更好的选择。两者在 Spring Boot 3.x 中可以共存,但不建议在同一个服务里混用——调试复杂度翻倍。
总结
- WebFlux 适合 IO 密集型、长连接、流式响应和高并发网关场景,不适合 CPU 密集或依赖阻塞库的业务
- Mono/Flux 是声明式 API,惰性求值,需要理解订阅才能执行
- 背压是响应式流的核心控制机制,使用
BaseSubscriber或limitRate()显式控制消费节奏,否则可能 OOM - 生产放
checkpoint()定位操作链,不要全局开Hooks.onOperatorDebug()(性能开销大) - 数据库必须用 R2DBC 或 MongoDB Reactive,不能混用 JDBC
- R2DBC 连接池并发控制、事务管理器配置、阻塞操作切线程是三大致命坑
- 切换 WebFlux 前务必评估:你的瓶颈是线程还是 CPU? 前者才值得入
- Spring Boot 3.x + 虚拟线程是 WebFlux 的最强替代方案,但流式场景下 WebFlux 仍不可替代
参考
参考:Spring WebFlux 官方文档 (https://docs.spring.io/spring-framework/reference/web/webflux.html)
参考:Reactor 3 Reference Guide (https://projectreactor.io/docs/core/release/reference/)
参考:Spring Boot 3.x 响应式编程实战
参考:R2DBC 官方文档 (https://r2dbc.io/)
参考:Netty 4.x 事件循环模型 (https://netty.io/wiki/reference-counted-objects.html)