当时团队遇到一个非常典型的问题:一个内部系统上线了文件批量导入和报表导出功能,平时并发不高,但每到月底就那么两三个小时,服务响应就从 200ms 涨到 5s 开外,甚至偶尔出现连接超时。排查了一圈,数据库慢查询没有,GC也没异常,问题基本都卡在等待 IO 上。那段时间我借着这个契机认真把 Spring Boot WebFlux 从头过了一遍,也踩了不少坑。这篇文章就按我自己的学习路径,把 Spring Boot WebFlux 的入门过程、关键原理、实操代码和排错经验完整记录下来,想给正在做技术选型或者刚开始接触响应式编程的开发者一个可参考的路线。
WebFlux 不是什么银弹,但它确实能解决一类很现实的问题:IO 密集场景下的线程浪费和高并发吞吐瓶颈。这篇文章从“为什么需要它”讲起,再到 Mono、Flux 这类响应式核心概念怎么理解,然后带你从零搭一个能跑的 WebFlux 工程,顺手接入关系数据库,最后说说测试调试和实战中那些手册里不会写的坑。无论你是刚接触响应式编程,还是已经在用传统 Spring MVC 想横向比较,都可以从里面找到可复用的结论。
1. 传统 Spring MVC 的痛点和 WebFlux 出现的理由
1.1 Servlet 线程模型到底卡在哪
先回到刚才那个场景。传统 Spring Boot 应用默认使用 Tomcat 作为内嵌容器,底层是经典的 Servlet 规范。Servlet 容器对每个请求的处理模型可以概括为一句话:一个请求进来,容器就从线程池里取一个线程,这个线程从头到尾负责这个请求的生命周期。
这个模型在 IO 密集场景下非常吃亏。比如一个 REST 接口要查询数据库、调用外部 HTTP 服务、写入文件,整个处理链路中真正消耗 CPU 的时间可能只有几毫秒,剩下的几十毫秒甚至几百毫秒都在等待下游响应。等待期间线程就挂起了,什么都不干,但依然占用着线程资源。Tomcat 默认的线程池大小是 200,也就是说同一时刻最多只能处理 200 个并发请求。第 201 个请求得排队,排队时间一长,用户感知到的就是“服务变慢”甚至“超时”。
很多人第一反应是调大线程数,比如调到 500 或者 1000。短期确实有效,但这不是免费的午餐。每个线程有自己的栈内存,默认 512KB 到 1MB,1000 个线程光栈内存就吃掉 1GB 左右。再加上线程切换的 CPU 开销,并发越高,操作系统调度越吃力,多线程反而是帮倒忙。我之前测试过一组数据:同样的接口,把线程池从 200 调到 1000,吞吐确实翻了一倍,但 CPU 多花在上下文切换上的比例明显上升,而且内存消耗涨了约 4 倍。这种方案不可持续。
1.2 换一个思路:少量线程配合事件循环
WebFlux 的核心出发点就是解决“线程被 IO 等待白白占住”的问题。它不采用一个线程处理一个请求的方式,而是用少量线程配合事件循环机制来推动处理流程。
事件循环是 Node.js 里非常成熟的一套模型,核心特点是非阻塞 IO。请求进来之后,系统向操作系统发起一个异步 IO 请求,然后这个线程立刻空闲出来去处理别的请求。等 IO 完成后,操作系统以事件的形式通知线程“数据准备好了”,线程再回来继续执行后面的逻辑。整个过程线程没有睡大觉,资源利用率高很多。
Spring Boot WebFlux 默认使用 Netty 作为内嵌容器,Netty 本身就是一个典型的事件驱动网络框架。默认情况下 Netty 的 worker 线程数等于 CPU 核数的两倍,比如一台 8 核机器,Worker 线程大概 16 个左右。这 16 个线程支撑几千个并发连接是完全没有问题的,因为它不是靠线程数量去扛并发,而是靠事件循环去流转请求。对比一下 Tomcat 线程池每请求占用一线程的模式,WebFlux 在处理高并发 IO 场景时的资源消耗优势非常明显。
1.3 WebFlux 适合什么场景,不适合什么场景
我理想中好的技术选型应该先明确边界,再谈实践。WebFlux 适合的场景有几个共同特征:高并发、IO 密集、下游依赖多、需要弹性伸缩。比如网关服务、消息推送服务、实时数据聚合服务、对外 API 聚合层,这些都是 WebFlux 的典型应用场景。原因很简单,这些服务的大部分时间都花在等待网络 IO、数据库查询、下游 API 响应上,非阻塞模型能省下大量线程资源。
不适合的场景也明确。一个是 CPU 密集型计算,比如复杂的图像处理、加解密运算、规则引擎推理——这些任务本身就在持续占用 CPU,用不用 WebFlux 差别不大,反而因为异步模型增加了理解和排错成本。另一个是项目组完全没有响应式编程经验的情况。WebFlux 的调试体验比传统 MVC 差是公认的,如果团队都是写同步代码出身,学习曲线会非常陡峭。在某些关系数据库操作复杂、强事务场景较多的系统中,R2DBC 对比 JPA 的成熟度依然有差距,盲目迁移容易把自己坑进去。我当时给团队的结论是:新增的高并发网关模块用 WebFlux,遗留的管理后台继续保持 Web MVC,微服务架构本来就不要求所有模块统一框架,能解决具体问题才是第一优先级。
2. 先搞懂响应式编程:Mono、Flux 与背压
2.1 响应式编程的思维转变:数据流与异步阶段
很多人在入门 WebFlux 时,最难的不是写 Controller,而是思维方式切换不过来。传统 MVC 的代码是顺序执行的:先查数据库拿到结果,再调用服务,最后拼接返回。响应式编程把这一套拆成了阶段式的异步流水线,每一阶段都返回一个发布者对象,后续操作通过操作符挂到这条流水线上。整个过程不会立刻执行,要等到有人订阅才会触发数据流动。
这个思维转变的典型类比是外卖订单。传统模式是你本人坐在店里等炒菜师傅炒完,端着菜走人;响应式模式是你下单之后拿到一个小票,继续去干别的事,等商家做好通过小票通知你取餐。小票就是 Mono 或 Flux 对象,商家就是 Publisher 发布者,取餐就是订阅。
2.2 Mono 与 Flux:发布者家族的两位主角
WebFlux 中接触最多的就是 Mono 和 Flux 这两个响应式类型。
Mono 表示异步序列中最多只有 0 个或 1 个元素,通常对应的是返回单个结果的操作。比如按主键查询一条记录、保存一条数据、判断记录是否存在,这些场景用 Mono 比较合适。Flux 表示异步序列中有 0 到 N 个元素,对应的是返回列表、数据流、消息流等操作。分页查询、批量查询、流式推送这些场景用 Flux。
下面用一段代码直观感受下它们的用法:
// 模拟查询订单,可能是空 Mono<Order> orderMono = orderRepository.findById(1001L); // 模拟查询某个用户的所有订单 Flux<Order> orderFlux = orderRepository.findByUserId(2024L); // 两者订阅后的行为差异:Mono 最多 emit 一个元素 orderMono.subscribe(order -> log.info("order = {}", order)); // Flux 订阅后会依次收到多个订单 orderFlux.subscribe(order -> log.info("order = {}", order.getId()));日常开发中,Mono 和 Flux 提供的操作符非常丰富,几乎可以解决所有常见的数据组合问题。map 用于同步转换,比如把实体对象转成 DTO;flatMap 用于把异步操作拍平,比如拿到用户 ID 后异步查询其订单;zip 用于并发组合多个发布者,等所有结果都到达后统一处理;onErrorResume 用于出错时返回一个兜底的发-布-者。掌握了这几个操作符,差不多就能应付大部分应用场景了。
2.3 背压机制:控制数据流速的阀门
响应式编程里还有一个躲不开的概念叫背压。简单说,背压就是订阅者告诉发布者“你慢点,我处理不过来”。Reactor 中背压的实现是通过 Subscription 的 request(n) 方法来控制请求元素个数的。
举一个现实例子。有一个批量同步接口,下游查询返回 Flux ,如果发布者一次性把十万条记录全部发射出来,订阅方的内存会瞬间被打满。加背压之后,订阅方每处理完一批,再向发布者申请下一批,这样数据流就能平滑推进。
使用 Flux 操作符时可以主动控制流的速度。limitRate(100) 表示每次预取 100 个元素,buffer(20) 表示把元素攒成一批再下发。另一个常见用法是使用 create 或 generate 创建自定义流时,通过 FluxSink 接收订阅方的背压请求:
Flux.create(sink -> { for (int i = 0; i < 1000; i++) { // 根据订阅方请求数量发射元素 sink.next(i); } sink.complete(); }, FluxSink.OverflowStrategy.BUFFER);背压的价值在于让系统不再单纯依赖内存大小来承担并发压力,而不是直接把数据全部塞给下游。设计响应式接口时,把背压纳入考虑是专业和非专业的明显分界线。
3. 从零搭建一个 Spring Boot WebFlux 项目:依赖、启动与第一个接口
3.1 项目初始化与依赖选择
我用的是 Spring Boot 3.3 版本,配合 JDK 17。WebFlux 在 Spring Boot 2.x 时代已经很成熟,但在 3.x 中是完全主推的方案,依赖使用也简化了不少。
新建一个 Maven 项目,核心依赖只需要引入一个 starter:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>如果项目需要访问数据库,再加 R2DBC 相关依赖;如果只写内存操作和模拟数据,上面这一个 starter 就够了。注意不要再引入 spring-boot-starter-web,WebFlux 和 Web MVC 同时存在于 classpath 中会导致路由冲突,Spring Boot 反而不知道听谁的,虽然可以通过配置强行规避,但入门阶段不要让这两个框架打架。
启动类和传统 Spring Boot 没有任何区别,依然是标准的 main 方法 + @SpringBootApplication:
@SpringBootApplication public class WebfluxDemoApplication { public static void main(String[] args) { SpringApplication.run(WebfluxDemoApplication.class, args); } }启动后观察日志,你会看到应用运行在 Netty 上而不是 Tomcat 上,端口默认仍然是 8080。这一点基本可以确认 WebFlux 已经成功启用。
3.2 用注解编写第一个响应式 Controller
WebFlux 完全支持 Spring MVC 风格的注解驱动方式,迁移成本非常低。写 Controller 时,返回类型从对象直接改成 Mono 或 Flux 即可。Spring Boot 框架会负责订阅发布者并把结果异步写回到客户端。
@RestController @RequestMapping("/api/users") public class UserController { @GetMapping("/{id}") public Mono<UserVO> getUser(@PathVariable Long id) { return userService.findById(id) .map(user -> userMapper.toVO(user)) .defaultIfEmpty(new UserVO()); // 为空时给个默认对象 } @GetMapping("/list") public Flux<UserVO> listUsers() { return userService.findAll() .map(user -> userMapper.toVO(user)); } }上面的代码没有一行阻塞逻辑,方法几乎瞬间返回,真正的查询动作在订阅之后由 Netty 事件循环线程异步执行。这种情况下,Controller 方法本身不会占用请求线程等待数据库,IO 来的慢也不影响其他请求处理,这就是响应式带来的实际收益。
3.3 路由式编程:另一种更灵活的写法
WebFlux 还支持函数式路由,这是很多人一开始容易忽略的特性。路由式编程把 HTTP 路由定义和请求处理逻辑拆成两部分,适合接口规则动态、需要精细控制路由的场景。
先定义一个 RouterFunction 的 Bean:
@Bean public RouterFunction<ServerResponse> userRoutes(UserHandler handler) { return RouterFunctions.route() .GET("/api/users/{id}", handler::getUser) .GET("/api/users", handler::listUsers) .build(); }再写一个 UserHandler 作为请求处理组件:
@Component public class UserHandler { public Mono<ServerResponse> getUser(ServerRequest request) { Long id = Long.valueOf(request.pathVariable("id")); return userService.findById(id) .flatMap(user -> ServerResponse.ok().bodyValue(user)) .switchIfEmpty(ServerResponse.notFound().build()); } }路由式编程的优势是把路由和业务逻辑分离,非常适合做统一网关或对外 API 层。不过注解式 Controller 在团队协作和理解成本上更友好,我一般建议新手优先掌握注解式写法再回头学路由式,不要纠结哪个“更高级”。
3.4 序列化输出的一点心得
响应式接口在序列化上和传统 MVC 大多一样,使用 Jackson 自动处理 JSON。唯一要注意的是 Flux 类型的返回值,如果只是正常返回列表,Flux 被订阅完成后会把所有元素收集起来统一序列化为 JSON 数组;如果想要流式输出,比如逐个推送 JSON 对象,需要配合 produces 指定 application/stream+json 或用 Server-Sent Events,否则客户端默认收到的还是一个完整数组。
后来我在做消息推送服务时,大量使用了 Flux t;SSE 让后端可以持续向浏览器推送离屏数据,前端只需要监听 EventSource,代码复杂度比 WebSocket 小很多。这也是 WebFlux 流式能力的一个非常实用的应用场景。
4. 操作数据库:R2DBC 的实际体验
4.1 为什么传统 JDBC 在响应式链路里会成为断点
如果你在 WebFlux 项目中引入 Spring Data JPA 去查数据库,接口返回的 Mono 照样能写,但底层驱动依然是阻塞式 JDBC。这个断点非常关键:查询发生时会把事件循环线程阻塞住,整个应用的所有请求都受影响。你会在日志里看到请求处理时间忽高忽低,并发一上来反而比传统 MVC 更差。
响应式应用访问数据库必须使用非阻塞驱动。对于关系型数据库,Spring Boot 3.x 已正式支持 R2DBC。它是一套基于响应式流规范的数据库驱动规范,通过事件驱动的连接管理和查询执行实现了真正的异步数据库访问。
4.2 引入 R2DBC 依赖与连接配置
我用 H2 数据库做本地演示时,依赖如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-r2dbc</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.r2dbc</groupId> <artifactId>r2dbc-h2</artifactId> <scope>runtime</scope> </dependency>application.yml 里的配置也和传统 JDBC 完全不同:
spring: r2dbc: url: r2dbc:h2:mem:///testdb username: sa password: pool: enabled: true max-size: 20 initial-size: 5R2DBC 的连接池和 JDBC 连接池并不是一种结构,它内部维护的是异步连接,通过依赖注入的方式自动提供给 Repository 使用。连接池的 max-size 要根据下游数据库连接数和系统并发度合理设置,并不是越大越好,R2DBC 本来就是轻量连接方案,搞太大反而增加管理和切换开销。
4.3 编写 Repository 与响应式查询
Spring Data R2DBC 的使用方式和 JPA 比较接近,写一个接口继承 ReactiveCrudRepository 即可:
public interface UserRepository extends ReactiveCrudRepository<User, Long> { Mono<User> findByUsername(String username); Flux<User> findByAgeGreaterThan(int age); }注意 R2DBC 默认不支持自动关联实体和复杂对象关系映射。它不像 JPA 那样管理一级缓存、懒加载、脏检查这些大而全的功能。R2DBC 更像一个轻量的反应式数据访问层,适合自己控制查询逻辑。例如要关联查询时,通常的做法是先查主表,拿到主键列表后再用 IN 查询子表,然后在内存中组装:
@Repository public class OrderRepository { private final DatabaseClient client; public OrderRepository(DatabaseClient client) { this.client = client; } public Flux<OrderDetail> findOrderDetailByUserId(long userId) { return userRepository.findById(userId) .flatMapMany(user -> client.sql("SELECT * FROM orders WHERE user_id = $1") .bind(0, user.getId()) .map((row, metadata) -> new OrderDetail( row.get("order_no", String.class), row.get("amount", BigDecimal.class))) .all()); } }DatabaseClient 是 R2DBC 的核心 API,比 Repository 更灵活,适合复杂查询场景。实际项目中 Repository 加 DatabaseClient 混合使用,能覆盖绝大部分需求。
4.4 事务与回滚的注意事项
R2DBC 的事务使用方式和 JPA 没有太大差异,同样用 @Transactional 注解,但底层实现已经是响应式的事务传播。需要注意方法内部必须保证所有操作都返回响应式类型,事务管理器才能正确调度它们的执行时机:
@Transactional public Mono<Void> transfer(Long fromId, Long toId, BigDecimal amount) { return accountRepository.findById(fromId) .flatMap(from -> accountRepository.findById(toId) .flatMap(to -> { from.debit(amount); to.credit(amount); return accountRepository.save(from) .then(accountRepository.save(to)) .then(); })); }如果方法内混用了阻塞式数据库操作或直接调用了 Future 的 get() 方法,事务管理器的异步协调就完全失效了,很容易出现“事务好像没有回滚”的诡异问题。记住一条铁律:响应式方法中不要混用阻塞操作。
5. 用 WebTestClient 写可靠的响应式接口测试
5.1 为什么 MockMvc 测不了 WebFlux 接口
传统项目我们习惯用 MockMvc 发起模拟 HTTP 请求来测试 Controller,但 WebFlux 应用的底层是 Netty 的事件循环,请求执行链路是异步且不阻塞的,MockMvc 那种“发起请求并同步等待结果”的方式在这里走不通。
Spring Boot 为 WebFlux 专门提供了 WebTestClient,它底层通过 WebClient 与运行中的服务通信,完全支持响应式语义。写测试时既可以使用真实端口,也可以使用 mock 方式绑定路由和请求处理器。
5.2 一个可运行的集成测试样例
先引入测试依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency>测试代码可以这样写:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class UserControllerTest { @Autowired private WebTestClient webTestClient; @Test void shouldReturnUserWhenUserExists() { webTestClient.get() .uri("/api/users/1") .exchange() .expectStatus().isOk() .expectBody() .jsonPath("$.id").isEqualTo(1); } @Test void shouldReturnUserList() { webTestClient.get() .uri("/api/users/list") .exchange() .expectStatus().isOk() .expectBodyList(UserVO.class) .hasSize(2); } }WebTestClient 的 expectStatus、expectBody 这类断言本质上也是异步的,它会等待响应式结果完成后做校验。测试速度和稳定性比传统 MockMvc 更好,因为它不依赖线程阻塞,测试环境配置也简单。
5.3 用 StepVerifier 验证数据流细节
WebTestClient 主要用来验证 HTTP 接口整体表现,但如果想精确验证 Mono 或 Flux 的发射顺序、报错处理、背压行为,需要用 Reactor 提供的 StepVerifier。
下面这段代码验证 Flux 是否按预期先发射 1、2、3,再执行 error:
Flux<Integer> flux = Flux.just(1, 2, 3) .concatWith(Flux.error(new IllegalStateException("boom"))); StepVerifier.create(flux) .expectNext(1) .expectNext(2) .expectNext(3) .expectErrorMessage("boom") .verify();StepVerifier 还能模拟背压场景,比如用 thenRequest 控制每次订阅请求的元素个数,从而验证代码在限流情况下是否表现正确。写异步测试时,我习惯把 StepVerifier 的 verify 方法坑也比较多,很容易因为遗漏 verify 导致测试并未真正断言数据流。
6. 实战中容易踩的坑和排查思路
6.1 在响应式链路里偷偷调用阻塞方法
WebFlux 最大的坑不是框架本身,而是开发者不自觉地把传统阻塞代码带进来。最常见的前三名是:
- 调用 block() 或 blockFirst() 等方法拿到结果再继续。
- 在 map 操作里做 Thread.sleep 模拟延时。
- 使用同步 JDBC 访问数据库或同步 HTTP 客户端。
这几种行为不分场景都会阻塞事件循环线程,一旦事件循环被堵住,整个服务都感受不到并发优势。之前我在排查一个 WebFlux 网关问题时,路由转发一直无响应,线程 dump 后发现 Netty 的 Worker 线程全部卡在了某个第三方 HTTP 客户端的同步 get() 上,原因就是有人在 Filter 里直接调用了同步请求。
排查思路也比较固定:先看现场线程 dump,搜索名字中包含 reactor-http-nio 的线程;如果大量线程停留在同一个调用栈而且指向某个 IO 操作,基本可以断定是阻塞调用。修复方式就是把同步调用改为 WebClient 异步调用,或者使用 Schedulers.boundedElastic() 把阻塞任务隔离出去。不过后者只作为过渡手段,本质上依然没有实现全链路异步。
6.2 原理:为什么 Mono.defer 和 lambda 懒加载很关键
响应式编程对时序的理解和命令式编程差异很大。Mono.just 和 Mono.defer 是学习时绕不开的一组对比。
Mono.just 在创建时就已经完成了参数求值。如果参数是一个有副作用的方法调用,那么即使后面没有订阅,副作用也已经发生了。Mono.defer 则延迟到每次订阅时才创建数据流,因此适合包装那些希望在订阅时才执行的计算。
// 假设 getCurrentUser() 每次调用都有副作用 Mono<User> userMono1 = Mono.just(getCurrentUser()); // 立即调用 Mono<User> userMono2 = Mono.defer(() -> Mono.just(getCurrentUser())); // 订阅时才调用在编写响应式代码时,如果发现某些逻辑在应用启动时就提前执行,而预期是请求到达时才做,很有可能就是创建发布者的时候没用 defer。尤其是取系统时间、读全局配置、连接会话这类状态相关的操作,一定要用 defer 包一层。
6.3 WebFlux 的安全上下文与跨线程问题
Spring Security 配合 WebFlux 时,安全上下文默认保存在响应式上下文里,且是和当前线程绑定的。如果业务代码里用 subscribeOn 切换了执行线程,原来线程上的 SecurityContext 信息并不会自动传递过去,查权限时可能会发现 Authentication 为空。
解决办法有两种。一种是在代码中手动取出来,再放到目标线程的上下文:
Mono<Authentication> authMono = ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication); authMono.flatMap(auth -> { return someReactiveService.doSomething(auth).subscriberContext(context -> context); })另一种是用 Spring Security 提供的 ReactiveSecurityContextHolder 直接在响应式链路上读取,避免手动传递。实际开发中我比较推荐后者,因为手动传递上下文太容易遗漏。理解这条线之后,再配合 @EnableWebFluxSecurity 就能把权限体系和响应式服务部署得比较稳。
6.4 异常处理:不要都在 Controller 里 try-catch
传统 MVC 的 @ControllerAdvice 能统一处理异常,WebFlux 也支持类似机制,但异步链路中异常处理更依赖操作符。
响应式流规范中错误是终止信号,一旦发生错误,流就结束了。如果只需要兜底,用 onErrorReturn 返回默认值;如果需要根据错误类型恢复出新的数据流,用 onErrorResume;如果只是记录日志不想中断,用 doOnError。关键原则是尽量在链路关键节点处理异常,而不是在最外层通过 try-catch 包裹。因为响应式流的错误不会像同步代码那样往调用栈上层传播,try-catch 根本抓不住异步错误信号。
我写过这样一个案例:R2DBC 查库时连接池耗尽,会抛出 PoolAcquireTimeoutException,如果在 Controller 层 try-catch 基本无效;正确做法是在 Repository 方法内部使用 onErrorResume 捕获这个特定异常并抛出自定义业务异常,最后由全局错误处理统一格式化返回给客户端。
6.5 配置线程池与资源隔离
WebFlux 底层默认线程配置对多数场景是够用的,但分层架构下的网关任务需要合理分配执行线程池。Reactor 中常用的调度器有:
- Schedulers.parallel():适合 CPU 密集任务,线程数按 CPU 核数。
- Schedulers.boundedElastic():内部维护弹性线程池,适合隔离阻塞任务。
- Schedulers.single():单线程调度,适合任务极轻量或要求串行的场景。
不过要区分一个概念:Netty 的 Worker 事件循环线程负责处理网络 IO,而业务代码如果用了 subscribeOn 或 publishOn,就会把任务切换到指定调度器执行。一个接口内部如果是纯计算逻辑且没有数据库访问,理论上不需要切换;如果有第三方阻塞调用,切到 boundedElastic 至少能避免影响事件循环,但全链路异步才是正解。这里我的建议是不要一开始就大量配置自定义线程池,用默认配置跑起来,再用压测工具看线程变化,缺什么补什么,过度设计往往是性能问题的催化剂。
7. 最后聊几句我的个人体会
把 Spring Boot WebFlux 从头到尾学完后回头看,我觉得它最值得被记住的价值不是“并发高了多少”,而是逼着你去重新理解 IO、线程、异步这些基础概念。很多写传统 MVC 写了几年的人都会有同感:平时接口调得好好的,一上 WebFlux 才发现自己根本没真正理解异步链路是怎么回事。
我目前在实际项目中采用的原则是:网关、消息推送、聚合查询类服务优先使用 WebFlux;内部管理后台、事务链复杂的老系统继续沿用 Web MVC。不为了新框架而新框架,也不因为遇到几个坑就直接否定它。在用 WebFlux 做真实项目时,尽量让团队先在一两个边界清晰的新模块上跑通全链路,包括安装、测试、部署、日志排错各环节都验证一遍,再考虑是否推广。毕竟对一个团队来说,运行稳定和维护成本往往比某个指标上的性能提升更重要。希望这篇入门经验能让你在接触响应式编程时少走一些我走过的弯路。