
WebFlux 这个东西说实话刚出现那两年争议很大。有人觉得它是未来有人觉得就是换个写法折腾自己直到我看过一个真实案例某个系统压测时请求量一起来Tomcat 线程池被打满CPU 没吃多少但请求全在排队平均响应时间肉眼可见地往上飙。换成 WebFlux 之后同样的硬件配置吞吐量直接翻了将近三倍。从那以后我就开始认真研究它越研究越发现很多人不敢用或者用不好不是因为它难而是因为不了解它的底层逻辑还在用写 Spring MVC 的思路去写响应式代码那必然处处踩坑。这篇内容不打算像官方文档那样从概念讲到配置我会直接把我实际做项目时的思考、选型理由、代码结构和排障经验都摆出来。适合谁看一是准备从 Spring MVC 过渡过来的后端开发二是已经在用 WebFlux 但经常遇到线程阻塞、数据查不出来、事务失效这类问题的人。看完之后你至少能明白 WebFlux 到底解决了什么问题、哪些场景适合用它、哪些场景千万别硬上以及上手时最容易被忽略的关键细节。1. 为什么选择 WebFlux技术选型背后的思考1.1 阻塞模型的问题到底出在哪传统的 Spring MVC 基于 Servlet 容器每个请求进来容器会从线程池里分配一个线程整个请求的处理过程——包括调用远程接口、查询数据库、读写文件——都由这个线程全程负责直到响应写完才释放。听起来很合理对吧问题在于大多数业务请求里真正的 CPU 计算时间可能只占 5%剩下 95% 都在等。等数据库返回结果等下游 HTTP 接口响应等 Redis 读取完成。那 95% 的时间里这个线程就是在空转、挂起、休眠。如果系统是 IO 密集型的比如网关、消息推送、聚合查询服务线程池很容易被这些“等待中”的线程占满。线程多了之后CPU 还要花大量时间做上下文切换系统整体吞吐量不升反降。常见调优手段是把线程池调大但线程本身占用内存调大了又会导致 GC 压力上升治标不治本。WebFlux 的思路完全不同。它把处理过程拆成一个个小任务当前任务需要等待 IO 时线程不闲着立刻去处理下一个任务。等 IO 结果回来了再通过回调或者事件通知继续往下走。这样少量线程就能支撑海量并发请求线程数不再和请求数成正比而是和 CPU 核心数相关。这个思路和 Node.js 很像但它运行在 JVM 上能直接利用 Java 生态里成熟的库和工具。1.2 响应式编程的核心前提很多人一上来就学 WebFlux 的 API结果被Mono、Flux、flatMap绕得头晕。我的建议是先理解“响应式编程是一种编程范式”再去看具体 API。它有三个核心前提理解了这三个后面的代码基本不用背。第一数据是“推送”的不是“拉取”的。传统写法是你调用一个方法方法执行完把结果返回给你这是你主动去要数据。响应式里你只是把“拿到数据之后要做什么”这件事告诉框架数据什么时候到、从哪个线程到都由框架决定。也就是订阅关系先行数据到达后再触发逻辑。第二一切都是异步的。方法调用不再阻塞等待结果而是立即返回一个Mono或Flux实际结果可能在几百毫秒甚至几秒之后才产生。异步带来了并发效率但也意味着你无法用传统try-catch捕获异常因为异常也变成异步流里的事件了。第三背压机制是天然的。消费者处理不过来时可以主动告诉生产者“你慢点发”而不是让数据无限堆积在内存里。这在 WebFlux 里是内置的能力Spring MVC 里则完全没有对应机制通常只能靠限流组件在入口处拦截。2. 核心原理拆解从 Reactor 到 WebFlux 的完整链路2.1 发布-订阅机制Mono 与 Flux 的底层关系WebFlux 底层的响应式流实现是 Project Reactor核心抽象就两个Mono表示 0 到 1 个元素的结果比如查询单条记录、发起一次 HTTP 请求Flux表示 0 到 N 个元素的结果比如查询列表、监听事件流。有人把它们类比成Optional和Stream虽然不完全准确但入门阶段可以这么理解。Mono和Flux本身只是数据流的“定义”什么也没做。真正的执行发生在你调用subscribe()的时候这就像在电影院买了一张电影票但电影还没开始放映观众都入座了屏幕才开始亮起来。一个常见的误解是拿到Flux后马上就去遍历它像处理List一样这是思维方式没转换过来。响应式流里的数据不是一次性全给到你的而是一个一个地推送过来你要做的是声明“每个元素到达时怎么处理”。发布者和订阅者之间还有一个重要的中间层叫Subscription它负责协调生产速率。默认情况下订阅者向发布者请求Long.MAX_VALUE个元素也就是有多少给多少。如果你想控制速率可以在订阅时指定初始请求数量request(n)这就是背压的入口。2.2 线程模型与事件循环WebFlux 默认运行在 Netty 之上Netty 的线程模型是典型的 Reactor 模型核心是 EventLoop 线程。EventLoop 线程内部维护了一个任务队列不断循环处理 IO 事件和定时任务。一个 EventLoop 会负责多个连接当一个连接发生读写事件时对应的回调方法会在这个 EventLoop 线程上执行。这里有个特别重要的特性同一个连接上的所有事件永远由同一个 EventLoop 线程处理。好处是你不用担心多线程并发修改同一个连接的状态坏处是如果你在事件回调里做了阻塞操作比如Thread.sleep()或者调用了一个同步的 JDBC 数据库访问这会把整个 EventLoop 线程都卡住。一个线程被卡住意味着这个线程上所有的连接都无法响应事故范围被放大了。所以 WebFlux 官方文档反复强调不要在响应式链路上使用阻塞调用。如果真的不可避免比如老项目里只有同步 JDBC 的Repository方法那必须把这段阻塞逻辑放到单独的线程池去执行然后用subscribeOn或者publishOn切换回响应式链路。实战中我还见过有人在doOnNext里直接调同步的短信发送接口结果短信服务延迟一次整个 WebFlux 服务都抖排查半天才定位到是这里的问题。2.3 与 Spring MVC 的核心差异简单列一个对照表方便你快速理解两者定位上的不同维度Spring MVCSpring WebFlux底层容器Tomcat、Jetty 等 Servlet 容器Netty、Undertow异步、Servlet 3.1 容器编程模型阻塞式一请求一线程响应式少量线程支撑高并发数据返回直接返回对象或ResponseEntity返回MonoT或FluxT数据库访问JPA / MyBatis同步 JDBCR2DBC / MongoDB Reactive / 响应式 Redis并发模型线程池按需增长EventLoop 事件驱动背压支持无原生支持这不是说 WebFlux 全面优于 Spring MVC。WebFlux 的响应式链路要求整条调用链都是非阻塞的如果你的数据库是 MySQL但公司 DBA 只允许用 JDBC 方式连接那 WebFlux 的优势就很难发挥出来反而徒增复杂度。还有一点WebFlux 的调试体验明显比 MVC 差响应式链路里报错时堆栈往往跨越多个异步阶段经常看不出真正的问题出在哪一行。如果你所在的团队平均技术水平一般对异步编程又不熟悉我建议先在一个非核心服务上试点别一上来就全部迁移。3. 从零搭建一个 WebFlux 服务实操记录3.1 工程配置与依赖选择Spring Boot 2.x 和 3.x 对 WebFlux 的支持都已经很成熟我用的是 Spring Boot 3.2 JDK 17 的组合。创建项目时只需要引入spring-boot-starter-webflux它会自动把 Netty 拉进来并把应用配置成 WebFlux 类型。如果你同时引入了spring-boot-starter-webSpring Boot 会默认走 MVC这一点非常容易踩坑很多人项目里历史依赖带出了starter-web结果RestController返回Mono也能跑但底层其实还是 MVC 的线程模型响应式的优势一点都没体现。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-r2dbc/artifactId /dependency注意这里我加了r2dbc依赖这是响应式操作关系型数据库的标准方式支持 MySQL、PostgreSQL、H2 等。如果你用了 Spring Data JPA那在 WebFlux 里是行不通的因为 JDBC 本身就是阻塞模型。数据层的选择在项目初期就要定好不然后面改起来成本很高。配置文件里我还会调整 Netty 相关参数比如最大连接数、空闲超时时间server: port: 8080 netty: max-connections: 10000 idle-timeout: 30s spring: r2dbc: url: r2dbc:mysql://localhost:3306/test_db username: root password: root pool: max-size: 20 initial-size: 5这里有个细节spring.r2dbc.pool.max-size不是配置的越大越好。响应式连接池里的连接数直接决定了并发上限如果设成 100数据库压力也不会小实际项目里我通常根据下游数据库的 QPS 能力反推一般 20 到 50 就够用了。3.2 用注解方式写第一个响应式接口WebFlux 支持两种开发方式注解式RestController和函数式RouterFunction。注解式和 Spring MVC 的写法几乎一样上手门槛最低适合大多数业务场景。下面是一个典型的例子RestController RequestMapping(/users) public class UserController { private final UserRepository userRepository; private final UserService userService; public UserController(UserRepository userRepository, UserService userService) { this.userRepository userRepository; this.userService userService; } GetMapping(/{id}) public MonoResponseEntityUser getUser(PathVariable Long id) { return userRepository.findById(id) .map(user - ResponseEntity.ok(user)) .defaultIfEmpty(ResponseEntity.notFound().build()); } GetMapping public FluxUser listUsers(RequestParam(defaultValue 0) int page, RequestParam(defaultValue 20) int size) { return userRepository.findAll() .skip((long) page * size) .take(size); } }注意这里的几个细节。第一返回类型必须是Mono或Flux不能直接返回User否则框架会把Mono当成普通对象序列化结果完全不对。第二defaultIfEmpty这个操作很关键它处理了查询不到数据时的空值场景避免前端收到一个null响应体。第三分页用了skip和take这是在数据流层面做截取不是数据库层面的分页数据量大的时候性能会受影响更好的做法是在 SQL 层就做 limit比如 R2DBC 的findAllBy方法里传分页参数。3.3 函数式端点RouterFunction 的玩法函数式端点是 WebFlux 独有的编程模型适合接口数量不多但路由规则变化频繁的场景比如网关、BFF后端为前端服务层。它把请求处理逻辑和路由配置彻底分离不依赖注解扫描启动速度更快路由规则可以动态构建。Configuration public class RoutingConfig { Bean public RouterFunctionServerResponse userRoutes(UserHandler userHandler) { return route() .GET(/api/users/{id}, userHandler::getUserById) .GET(/api/users, userHandler::listUsers) .POST(/api/users, userHandler::createUser) .PUT(/api/users/{id}, userHandler::updateUser) .DELETE(/api/users/{id}, userHandler::deleteUser) .build(); } }对应的UserHandler是一个普通类方法入参是ServerRequest返回MonoServerResponseComponent public class UserHandler { private final UserService userService; public UserHandler(UserService userService) { this.userService userService; } public MonoServerResponse getUserById(ServerRequest request) { Long id Long.parseLong(request.pathVariable(id)); return userService.getUserById(id) .flatMap(user - ServerResponse.ok().bodyValue(user)) .switchIfEmpty(ServerResponse.notFound().build()); } }我个人更推荐在复杂业务中使用注解式简单直观在网关类项目中使用函数式因为路由规则往往是从数据库或者配置中心动态加载的函数式构建路由更灵活。还有一种折中方案是两者混用一个工程里既有RestController也有RouterFunction它们可以共存不会冲突。这个方案在团队协作时比较实用不同成员可以根据自己的偏好各写各的。3.4 数据层接入 R2DBC响应式数据库访问R2DBCReactive Relational Database Connectivity是响应式访问关系数据库的标准。如果你的数据源是 MongoDB直接用 Spring Data MongoDB Reactive 即可如果是 MySQL就绕不开 R2DBC。它的使用方式和 Spring Data JPA 很像但有一些显著区别需要注意。第一Repository 接口的返回值必须写成Mono或Flux。第二自定义查询用Query注解写法基本和 JPA 一致。第三事务管理用Transactional但必须配合 R2DBC 的响应式事务管理器使用。看一个完整例子public interface UserRepository extends ReactiveCrudRepositoryUser, Long { Query(SELECT * FROM users WHERE age :age) FluxUser findUsersOlderThan(Param(age) int age); MonoBoolean existsByUsername(String username); }这里特别提醒一下ReactiveCrudRepository里的save()方法返回的是MonoS不是同步保存。如果你脑子还停留在 JPA 时代很容易写出这样的代码User saved userRepository.save(user).block();block()会强制把响应式流转换成阻塞等待这在 WebFlux 服务里是禁忌。它会让当前线程空转等待数据库结果EventLoop 线程一旦被 block整个服务的并发能力立刻下降。我在代码评审时看到过太多次这个写法基本都是因为写代码的人对响应式不熟悉顺手就调了block()。如果你发现自己要频繁调用block()要么是设计思路有问题要么这个服务根本不适合用 WebFlux。数据层的另一个坑是R2DBC 默认不会自动建表。ORM 时代大家习惯了ddl-auto: update但 R2DBC 没有这个能力你需要自己维护 SQL 建表脚本或者用 Flyway 这类迁移工具管理表结构。我第一次用 R2DBC 时就是因为没建表启动正常但一查数据就报表不存在折腾了半小时才意识到问题。4. 生产环境实践安全、调用链、限流与监控4.1 与 Spring Security 集成响应式安全配置Spring Security 在 WebFlux 下的用法和 MVC 下差异不小。最大的差异是传统基于HttpSecurity的配置方式在响应式环境下不可用你需要改用SecurityWebFilterChain和ReactiveAuthenticationManager。整体思路没变但 API 完全不同。先看一个最基础的路由放行配置Configuration EnableWebFluxSecurity public class SecurityConfig { Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http .csrf(ServerHttpSecurity.CsrfSpec::disable) .authorizeExchange(exchanges - exchanges .pathMatchers(/api/public/**).permitAll() .pathMatchers(/api/admin/**).hasRole(ADMIN) .anyExchange().authenticated()) .oauth2Login(Customizer.withDefaults()) .build(); } }注意这里用的是authorizeExchange不是 MVC 里的authorizeRequests。如果你在 WebFlux 项目里照着 MVC 的配置写会发现方法根本不存在这是新手最容易卡住的地方。自定义认证逻辑时需要实现ReactiveAuthenticationManagerComponent public class CustomReactiveAuthenticationManager implements ReactiveAuthenticationManager { private final UserService userService; public CustomReactiveAuthenticationManager(UserService userService) { this.userService userService; } Override public MonoAuthentication authenticate(Authentication authentication) { String username authentication.getName(); String password authentication.getCredentials().toString(); return userService.login(username, password) .map(user - new UsernamePasswordAuthenticationToken( user.getUsername(), null, user.getRoles().stream() .map(role - new SimpleGrantedAuthority(ROLE_ role)) .toList() )); } }实际项目中JWT 的校验逻辑也建议放在这里内层不要再用拦截器处理避免和 Spring Security 的过滤链重复。响应式环境下的关键原则是所有和认证、鉴权相关的操作都要返回Mono不能用同步的UserDetailsService。4.2 WebClient响应式 HTTP 客户端与调用链传递如果说 WebFlux 是服务端响应式框架那WebClient就是客户端响应式框架。它替代了传统的RestTemplate在 WebFlux 场景下是必选项。RestTemplate本身是阻塞的你在 WebFlux 服务里调用外部 API 时如果用了它等于把之前省下来的并发优势全部抵消了。WebClient的典型用法Service public class PaymentClient { private final WebClient webClient; public PaymentClient(WebClient.Builder webClientBuilder) { this.webClient webClientBuilder .baseUrl(https://payment.example.com) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); } public MonoPaymentResult createPayment(PaymentRequest request) { return webClient.post() .uri(/api/payments) .bodyValue(request) .retrieve() .bodyToMono(PaymentResult.class); } }在微服务架构下WebClient通常是集成在 Spring Cloud OpenFeign 或者 Spring Cloud Gateway 里的。WebFlux 服务作为上游服务调用下游时需要在请求头里传递 TraceId、UserId 等上下文信息。WebClient支持自定义ExchangeFilterFunction可以用它统一注入这些 HeaderComponent public class HeaderExchangeFilter implements ExchangeFilterFunction { Override public MonoClientResponse filter(ClientRequest request, ExchangeFunction next) { String traceId TraceContext.getTraceId(); String userId SecurityContextHolder.getContext().getAuthentication() ! null ? SecurityContextHolder.getContext().getAuthentication().getName() : anonymous; ClientRequest newRequest ClientRequest.from(request) .header(X-Trace-Id, traceId) .header(X-User-Id, userId) .build(); return next.exchange(newRequest); } }这里有个细节在 WebFlux 环境下SecurityContextHolder默认不是线程绑定的而是用ReactiveSecurityContextHolder传递上下文。如果你用传统的SecurityContextHolder.getContext()获取认证信息经常会拿到 null因为每个异步阶段都可能切换线程。正确做法是在响应式链路上获取比如在ReactiveSecurityContextHolder.getContext()里拿到Authentication。4.3 限流与背压处理WebFlux 原生支持背压但这不意味着我们可以不做限流。背压是消费者给生产者反馈的机制它的作用是防止消费者被数据洪流冲垮。但如果上游生产者自己都不知道要发多少数据或者数据是外部请求触发的你仍然需要限流。常见的限流方案有两种一种是基于 Redis 的分布式限流适合多实例部署一种是应用内限流适合单机场景。在 WebFlux 中可以用Bucket4j或者Resilience4j来实现限流。Resilience4j对响应式支持得很好自带RateLimiter配合reactor.extra可以很方便地集成。Configuration public class RateLimitConfig { Bean public RateLimiter rateLimiter() { return RateLimiter.of(api-limiter, RateLimiterConfig.custom() .limitForPeriod(100) .limitRefreshPeriod(Duration.ofSeconds(1)) .build()); } } RestController public class OrderController { private final RateLimiter rateLimiter; public OrderController(RateLimiter rateLimiter) { this.rateLimiter rateLimiter; } GetMapping(/api/orders) public MonoResponseEntityFluxOrder listOrders() { return Mono.fromSupplier(() - rateLimiter.acquirePermission()) .flatMap(acquired - { if (!acquired) { return Mono.just(ResponseEntity.status(429).build()); } return Mono.just(ResponseEntity.ok(orderService.listOrders())); }); } }这种写法有个小问题限流判断是同步的如果一瞬间大量请求进来依然会占用少量 EventLoop 线程做判断。更稳妥的做法是用ReactiveRateLimiter但引入额外的依赖。对于大多数项目上面的方案已经够了。另外从业务角度考虑限流触发时的响应最好带一个Retry-After头方便调用方做重试决策这个细节很容易被忽略。5. 常见问题与排查实录5.1 数据流不触发忘记订阅导致的“假死”响应式流是惰性的没有订阅者就不会执行。这句话理论说了一百遍但实际写代码时很多人还是会在PostConstruct或者某个异步任务里写userRepository.findAll() .map(user - userService.syncUser(user));这段代码不会执行任何数据库查询也不会发起任何同步。另外在测试时也经常遇到类似情况写出来的单元测试用例没有调用subscribe()或者block()结果测试日志里看不到任何断言输出还以为是代码 bug其实就是流没有启动。测试用的写法应该是StepVerifier.create(userRepository.findAll()) .expectNextCount(3) .verifyComplete();StepVerifier是 Reactor 自带的测试工具它本身就是订阅者能帮你触发流执行同时验证数据内容和完成信号。生产代码里尽量不要手动调用subscribe()去触发业务逻辑而应该让框架通过GetMapping等入口自动触发。如果确实需要在某个异步任务里触发那要非常小心异常处理流里的异常不会自动打日志需要用doOnError打印。5.2 阻塞操作拖垮 EventLoop 线程这是 WebFlux 实战中影响最大、最难排查的一类问题。前面说过EventLoop 线程数量有限一旦阻塞就是连锁反应。典型场景有在flatMap里调用同步 JDBC、在doOnNext里执行Thread.sleep()、在map里调用远程同步接口。我用一个真实案例来说。某个服务上线后高峰期偶发大量请求超时监控看 CPU 使用率低得异常但 Netty 线程池全部处于WAITING状态。排查时抓线程栈发现好几个 EventLoop 线程都卡在同一个RestTemplate.exchange()方法上确定是代码里某个map操作内部调用了同步的外部认证接口。这个外部接口平时耗时 200 毫秒偶尔抖动到 5 秒结果把 EventLoop 里的十几个连接全部拖住影响了所有经过这个 Netty 线程的请求。解决方法是把阻塞调用扔到专用线程池Mono.fromCallable(() - synchronousAuthService.getToken()) .subscribeOn(Schedulers.boundedElastic()) .map(token - buildRequest(token));boundedElastic是专门为阻塞操作准备的线程池能接受一定程度的阻塞但不会无限创建线程。原则是能不用尽量不用真要用就必须subscribeOn隔离绝不能直接在响应式链路上裸调。5.3 响应式事务的边界问题Transactional在 WebFlux 里经常失效原因是事务要绑定到当前线程但 WebFlux 的操作是跨线程的。Spring 官方提供了Transactional注解支持 R2DBC 事务但前提是事务管理器的类型匹配。如果整个项目只用了 R2DBC配置EnableTransactionManagement后在Mono链路上的Transactional是可以生效的但一旦链路上切换了线程池事务的传播就会中断。实际项目中我更推荐在 Service 层用编程式事务Service public class OrderService { private final TransactionalOperator transactionalOperator; private final OrderRepository orderRepository; public OrderService(TransactionalOperator transactionalOperator, OrderRepository orderRepository) { this.transactionalOperator transactionalOperator; this.orderRepository orderRepository; } public MonoOrder createOrder(Order order) { return transactionalOperator.transactional( orderRepository.save(order) .flatMap(savedOrder - orderRepository.updateStock( savedOrder.getProductId(), savedOrder.getQuantity())) .map(stockUpdated - savedOrder) ); } }TransactionalOperator.transactional()会把传入的Mono或Flux整个包裹进一个事务里如果中途发生异常会自动回滚。这样做的最大好处是事务边界一目了然而且不会出现注解失效的玄学问题。5.4 排查工具与手段WebFlux 的排查难度确实比 MVC 高但有一些工具能显著降低难度。首先是 Reactor 自带的Hooks.onOperatorDebug()在开发环境启用后异常堆栈会包含完整的操作符调用链信息定位问题能省一半时间。生产环境不建议开启会有额外性能开销。其次是响应式上下文。如果你在链路里需要传递用户信息、TraceId不要用全局静态变量要使用contextWrite写入上下文再通过deferContextual或者ReactiveSecurityContextHolder读取。这样在异步切换线程时数据不会丢失。最后是监控指标。引入micrometer的响应式相关指标比如reactor_netty_*、r2dbc_pool_*配合 Prometheus 告警能从宏观角度快速发现线程池异常和连接池耗尽的情况。这些指标在出问题时能提供关键线索比对着代码一行行看快得多。结尾说点实在的根据我个人的实际项目经验WebFlux 不是万能银弹也不是用来炫技的框架。如果你的业务是典型的 CRUD 场景并发量不高数据库又是 Oracle、SQL Server 这类 R2DBC 支持不完善的关系型数据库老老实实用 Spring MVC 就好维护成本低得多团队招人也容易。反过来如果你的服务要支撑高并发请求或者你正在做一个 API 网关、实时推送服务、聚合查询服务那 WebFlux 的优势非常明显。但用之前必须想清楚一件事响应式编程改变的不只是 API 写法而是整个团队的思维方式。代码评审时最常见的问题不是写不出Mono和Flux而是写的时候依然在用同步思路思考。如果你决定上手我的建议是先用一个小项目练手把注解式接口、函数式路由、R2DBC、WebClient 这一整套流程跑通再去看官方文档里的背压机制和调度器原理。遇到问题不要急着上网搜“WebFlux 为什么不执行”先问自己“我的流有没有被订阅”“我的链路里有没有阻塞操作”“我的数据源是不是响应式的”这三个问题基本能排除掉八成以上的坑。最后再分享一个小技巧在开发阶段全局开启Hooks.onOperatorDebug()同时把日志级别调到 DEBUG你会看到响应式链路每一步的执行情况。等系统稳定了再关掉调试 Hook接好指标监控。这个组合拳能帮你平稳度过大多数响应式项目的初期阶段。