为什么越来越多人使用WebFlux?

0 阅读13分钟

前言

从同步阻塞到异步非阻塞,Java Web开发的又一次革命

最近在做技术选型评审的时候,发现了一个非常有意思的现象——越来越多的新项目在技术选型时,把Spring WebFlux写进了方案里

有个小伙伴跟我说:“三哥,我看了网上的文章,有人说WebFlux性能炸裂,有人说WebFlux学习曲线太陡,还有人说要回归同步编程。我都不知道该信谁了。”

这个问题问得很到位。

WebFlux从Spring 5.0引入到现在已经快10年了,关于它的讨论从来没有停止过。

有人说它是“未来”,有人说它是“花架子”。

那真相到底是什么?

今天这篇文章就专门跟大家一起聊聊WebFlux,希望对你会有所帮助。

更多项目实战在我的技术网站:susan.net.cn/project

一、WebFlux到底要解决什么问题?

有些小伙伴在工作中可能会说:“我用Spring MVC用了好多年了,感觉也挺好的啊,为什么要学WebFlux?”

在回答这个问题之前,我们先看一个典型的Spring MVC控制器:

@RestController
public class OrderController {
    
    @GetMapping("/order/{id}")
    public Order getOrder(@PathVariable Long id) {
        // 调用外部接口获取数据,可能需要2秒
        User user = userService.getUser(order.getUserId());  // 线程在这里阻塞2秒!
        return order;
    }
}

问题出在哪里?

当请求到达服务器时,Servlet容器(如Tomcat)会从线程池中分配一个工作线程来处理这个请求。

在这个线程执行userService.getUser()的整整2秒钟里,这个线程什么也做不了,只能空转、等待。

它无法去处理其他已经到达的请求。

如果同一时间有1000个这样的并发请求,Tomcat就需要准备至少1000个线程来应对。

每个线程都消耗约1MB的栈内存,当线程数超过物理核心承载能力,大量的时间将浪费在线程上下文切换上,最终可能因资源耗尽而崩溃。

这就是 “一个请求,一个线程”  的阻塞模型在I/O密集型场景下的天然瓶颈。

我们投入了大量资源(线程),仅仅是为了“等待”,而不是“计算”。

image.png

核心问题:线程在等待I/O时完全空闲,但资源却被占着不放。

有些小伙伴可能已经通过增大线程池、服务拆分等方式缓解了这个问题,但这本质上是“用资源换吞吐量”,并非最优解。

当你的系统需要支撑数万并发连接时,这种“堆线程”的方式迟早会遇到天花板。

WebFlux要解决的问题,就是用更少的资源,支撑更高的并发

二、异步非阻塞 + 响应式流

WebFlux的哲学截然不同。

它源于响应式编程范式,核心目标是:用少量、固定的线程,处理大量并发请求

如何做到?

答案是事件驱动 + 异步非阻塞I/O

它不再让线程傻等,而是告诉系统:“我去做点别的,等数据准备好了,你再回调通知我。”

2.1 线程模型对比

这是理解WebFlux最关键的一张图:

对比维度Spring MVCSpring WebFlux
编程模型命令式 / 同步阻塞声明式 / 异步非阻塞 + 响应式流
线程模型Thread-per-Request(每个请求一个线程)Event-Loop + 少量线程(Netty默认)
I/O模型阻塞式I/O非阻塞式I/O
并发能力线程池大小决定(通常200-500)理论上可达数万~数十万连接
背压支持原生支持(Reactive Streams规范)
默认服务器Tomcat / JettyNetty(或Undertow)

WebFlux的线程模型完全是另一种思路

截屏2026-07-25 10.09.34.png

少量线程通过事件循环处理所有请求,I/O操作不阻塞线程,完成后通过回调通知。单个线程可以处理数万并发连接

直观对比:处理10,000个长连接时,WebFlux的CPU利用率比同步架构低40% ,内存消耗减少25%

在万级并发场景下,基于Netty的WebFlux应用相比传统Servlet容器可减少30%-50%  的内存消耗,同时保持更稳定的P99延迟。

2.2 Reactor与Mono/Flux

理解WebFlux的第一道坎,是理解Project Reactor的两个核心类型:

  • Mono:代表0或1个结果的异步序列。可以理解为一个“未来可能到来的单个数据包”的承诺。
  • Flux:代表0到N个结果的异步序列。
// 返回单个结果用Mono
@GetMapping("/user/{id}")
public Mono<User> getUser(@PathVariable Long id) {
    return userRepository.findById(id);  // 异步查询,立即返回Mono
}

// 返回多个结果用Flux
@GetMapping("/users")
public Flux<User> getUsers() {
    return userRepository.findAll();  // 异步查询,立即返回Flux
}

关键理解:方法执行后立即返回,不等待数据就绪

真正的数据会在未来某个时刻通过Mono/Flux传递过来。

三、从零搭建一个WebFlux应用

光说不练假把式。

我们花5分钟搭一个WebFlux应用看看。

3.1 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

不需要spring-boot-starter-web ,两者是互斥的。WebFlux默认使用Netty作为服务器。

3.2 响应式Controller

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    // 返回单个用户
    @GetMapping("/{id}")
    public Mono<UsergetUser(@PathVariable Long id) {
        return userService.findById(id)
            .switchIfEmpty(Mono.error(new UserNotFoundException(id)));
    }
    
    // 返回用户列表
    @GetMapping
    public Flux<UsergetUsers(@RequestParam(defaultValue = "0") int page,
                                @RequestParam(defaultValue = "20") int size) {
        return userService.findAll(page, size);
    }
    
    // 创建用户
    @PostMapping
    public Mono<UsercreateUser(@RequestBody Mono<User> userMono) {
        return userMono.flatMap(userService::save);
    }
}

代码拆解

  • 方法返回Mono<User>Flux<User>不等待数据就绪,立即返回
  • flatMap操作用于处理异步嵌套,避免“回调地狱”
  • switchIfEmpty提供响应式的空值处理

3.3 响应式Service

@Service
public class UserService {
    
    private final ReactiveUserRepository userRepository;
    
    public UserService(ReactiveUserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public Mono<User> findById(Long id) {
        return userRepository.findById(id);
    }
    
    public Flux<User> findAll(int page, int size) {
        return userRepository.findAll()
            .skip((long) page * size)
            .take(size);
    }
    
    public Mono<User> save(User user) {
        return userRepository.save(user);
    }
}

3.4 响应式数据访问(R2DBC)

WebFlux配合R2DBC(Reactive Relational Database Connectivity)才能实现端到端的非阻塞:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
    <groupId>io.r2dbc</groupId>
    <artifactId>r2dbc-postgresql</artifactId>
</dependency>
@Repository
public interface ReactiveUserRepository 
        extends ReactiveCrudRepository<User, Long> {
    
    Mono<User> findByEmail(String email);
    Flux<User> findByAgeGreaterThan(int age);
}

关键理解:如果没有R2DBC,只用WebFlux + 传统JDBC,数据库访问仍然是阻塞的,端到端的非阻塞就断在了数据库这一层。要让整个链路非阻塞,数据库访问也必须是非阻塞的。

四、WebClient:响应式的HTTP客户端

WebFlux提供了WebClient作为响应式的HTTP客户端,替代传统的RestTemplate:

@Service
public class OrderService {
    
    private final WebClient webClient;
    
    public OrderService(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder
            .baseUrl("http://user-service")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();
    }
    
    public Mono<User> getUserWithOrders(Long userId) {
        // 并行调用两个服务
        Mono<User> userMono = webClient.get()
            .uri("/api/users/{id}", userId)
            .retrieve()
            .bodyToMono(User.class);
            
        Flux<Order> ordersFlux = webClient.get()
            .uri("/api/orders?userId={id}", userId)
            .retrieve()
            .bodyToFlux(Order.class);
            
        // 合并结果
        return userMono.zipWith(ordersFlux.collectList())
            .map(tuple -> {
                User user = tuple.getT1();
                user.setOrders(tuple.getT2());
                return user;
            });
    }
}

关键点:两个外部调用是并行执行的,而不是串行。zipWith操作符会等待两个异步结果都返回后,再合并处理。

如果用RestTemplate,这两个调用是串行的,总耗时是两者之和;用WebClient,总耗时是两者中的最大值。

五、为什么越来越多的人使用?

5.1 云原生时代的资源效率革命

在Kubernetes集群中,每个Pod的资源配额是有限的。WebFlux应用可以用更少的内存和CPU支撑更高的并发。

真实数据:采用响应式架构的微服务集群在同等硬件条件下,吞吐量提升3-5倍,延迟降低60%以上。某行业调研显示,越来越多的企业正在将WebFlux引入生产环境。

5.2 流式数据处理与实时推送

WebFlux原生支持Server-Sent Events(SSE)  和WebSocket,适合实时数据推送场景:

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<StockPrice> streamStockPrices() {
    return stockService.priceStream()
        .delayElements(Duration.ofSeconds(1));  // 每秒推送一次
}

金融行情、IoT设备数据、实时监控等场景,WebFlux是天然的选择。

5.3 背压机制:防止系统过载

背压(Backpressure)是响应式流规范的核心特性。当消费者处理速度跟不上生产者时,消费者可以主动告诉生产者“慢一点”  。

Flux.range(11000000)
    .onBackpressureBuffer(1000)  // 缓冲1000个元素
    .subscribe(new BaseSubscriber<Integer>() {
        @Override
        protected void hookOnNext(Integer value) {
            // 处理一个
            request(1);  // 处理完一个再请求下一个
        }
    });

传统Servlet模型没有背压机制,生产者可能压垮消费者。WebFlux的背压机制在流式/大数据场景下具有碾压性优势。

5.4 GraalVM原生镜像支持

2025年的Spring生态中,WebFlux与GraalVM原生镜像的兼容性得到显著提升,启动时间优化至100ms以内

这对于Serverless和快速弹性伸缩场景意义重大——100ms的启动时间意味着Pod可以在几秒内完成扩容。

六、缺点与适用场景

6.1 优点

1. 极高的资源效率

WebFlux用少量线程处理大量并发,在云原生环境下意味着更少的Pod、更低的内存占用、更低的云成本。

在万级并发场景下,WebFlux可减少30%-50%  的内存消耗,同时保持更稳定的P99延迟。

Netty默认的Event Loop线程数通常等于CPU核心数,而Tomcat的线程池可能达到数百甚至上千——一个WebFlux应用在峰值负载下的线程数,可能只是MVC应用的十分之一

2. 流式数据处理与实时推送

WebFlux原生支持Server-Sent Events(SSE)  和WebSocket,金融行情、IoT设备数据、实时监控等场景天然适合。在传统Servlet模型中实现流式推送,需要对异步支持做很多额外处理。

3. 背压机制(Backpressure)

当消费者处理速度跟不上生产者时,消费者可以主动告诉生产者“慢一点”。传统Servlet模型没有背压机制,生产者可能压垮消费者。这在流式/大数据场景下具有碾压性优势。

4. 端到端的非阻塞

从Web层到数据访问层(配合R2DBC)全链路非阻塞,所有环节都运行在Netty的事件循环上,避免了线程切换开销。

5. WebClient并行调用能力

WebClient支持多个外部服务调用的并行执行,用zipWithmerge等操作符组合多个异步请求,总耗时从“串行之和”变成“并行最大值”。

6. GraalVM原生镜像支持

2025年的Spring生态中,WebFlux与GraalVM原生镜像的兼容性显著提升,启动时间优化至100ms以内,对Serverless和快速弹性伸缩场景意义重大。

7. 与Spring生态深度融合

WebFlux是Spring生态的一部分,与Spring Boot、Spring Security、Spring Data、Spring Cloud等组件无缝集成,无需引入额外框架。

6.2 缺点

1. 学习曲线陡峭需要理解响应式思维、Mono/Flux操作符、背压机制等新概念。从命令式编程转向响应式范式,需要完成从同步到异步、从状态到流、从阻塞到订阅的认知跃迁。

2. 调试难度高响应式代码的堆栈跟踪不像同步代码那么直观,flatMap链中的异常可能跨越多个操作符。不过Spring提供了BlockHound等工具来辅助检测线程阻塞问题。

3. 生态适配仍需时间部分传统JDBC、ORM库没有响应式版本,需要寻找R2DBC等替代方案。端到端的非阻塞需要整个技术栈都支持响应式——如果数据库访问仍然是阻塞的,WebFlux带来的收益就会被抵消

4. 简单CRUD场景杀鸡用牛刀对于简单的CRUD应用,WebFlux带来的性能提升可能不足以抵消其复杂性。而且阻塞I/O模型在虚拟线程的支持下,已经能更轻松地达到接近WebFlux的并发能力。

5. 默认配置调优空间大但要求更高WebFlux的默认配置适合中等负载,但在高并发场景下需要根据实际业务特征调优Netty参数(如maxConnectionsidleTimeouteventLoopThreads等),这对运维团队提出了更高的要求。

6.3 适用场景

场景推荐程度理由
API网关强烈推荐高并发、I/O密集、服务聚合
实时数据推送强烈推荐SSE/WebSocket原生支持
微服务间调用强烈推荐WebClient并行调用,延迟降低明显
流式数据处理强烈推荐背压机制天然适配
Serverless/FaaS推荐GraalVM原生镜像启动快
传统CRUD应用需评估收益不明显,学习成本高
CPU密集型计算不推荐WebFlux优势在I/O密集型,CPU密集型反而增加了调度开销

最佳实践判断:如果你的应用是I/O密集型(大量数据库查询、外部API调用、文件操作),WebFlux能发挥最大价值。如果是CPU密集型,WebFlux的优势不明显,甚至可能因为操作符链的额外开销而略慢于同步方案。

最佳实践判断:如果你的应用是I/O密集型(大量数据库查询、外部API调用、文件操作),WebFlux能发挥最大价值。如果是CPU密集型,WebFlux的优势不明显。

七、虚拟线程来了

2026年,Java生态中出现了一个重要的新变量——虚拟线程(Virtual Threads,Project Loom)  。

虚拟线程让同步阻塞代码也能以极低的成本支撑高并发。

用Tomcat + 虚拟线程,开发者可以用熟悉的同步编程模型,获得接近WebFlux的并发能力。

那WebFlux会被取代吗?

目前来看,两者是互补的关系。

虚拟线程适合大部分传统业务场景,让同步代码也能支撑高并发。

WebFlux在流式数据处理、背压控制、细粒度线程调度等场景下仍有不可替代的优势。

2026年的技术选型不再是“非此即彼”,而是根据具体场景选择最合适的方案。

更多项目实战在我的技术网站:susan.net.cn/project

八、写在最后

回到最初的问题:为什么越来越多的人使用WebFlux?

第一,资源效率的革命。  WebFlux用少量线程处理大量并发,在云原生环境下意味着更少的Pod、更低的内存占用、更低的云成本。在万级并发场景下,WebFlux可减少30%-50%  的内存消耗。

第二,流式数据处理的天然适配。  SSE、WebSocket、背压机制——这些能力在传统Servlet模型中要么不支持,要么实现起来非常别扭。WebFlux让流式数据处理变得优雅。

第三,云原生时代的必然选择。  在Serverless、快速弹性伸缩的场景下,WebFlux + GraalVM原生镜像的启动时间可优化至100ms以内。这种启动速度,传统JVM应用难以企及。

当然,WebFlux不是银弹。

学习曲线陡峭、调试难度高、生态还在完善——这些短板客观存在。

而且2026年虚拟线程的出现,让同步编程也有了高并发的能力。

我的建议是:如果你的应用是I/O密集型、需要支撑高并发、或者需要流式数据处理能力,WebFlux值得你花时间深入学习。

如果只是简单的CRUD应用,传统Spring MVC + 虚拟线程可能是更务实的选择。

技术选型没有标准答案。

理解自己的业务场景,选择最匹配的技术方案,才是架构师真正该做的事。