Spring WebFlux响应式编程入门:核心概念、线程模型与实战搭建
1. 从Servlet到Reactive为什么要在Spring Boot里引入WebFlux很多同学第一次接触WebFlux是因为项目里有人提了一句“咱们这个接口并发上不去要不要试试响应式”。然后打开Spring Initializr勾上Spring Reactive Web生成项目写了个Controller返回Mono 跑起来发现——好像跟以前写Spring MVC也没什么区别。于是心里犯嘀咕这东西到底解决什么问题我当初也是这么想的。真正让我理解WebFlux价值的是一个很具体的场景一个聚合接口需要同时调用三个下游服务每个平均耗时200ms。用传统的Spring MVC同步写法三个调用串行执行总耗时600msTomcat线程池里就有一个线程被占住600ms。当QPS上来之后线程池很快被打满后续请求全部排队。而WebFlux的做法是三个调用同时发起总耗时取决于最慢的那个大约200ms出头而且整个过程不占用额外的线程去“等待”。这就是WebFlux的核心价值用更少的资源处理更多的并发连接尤其是I/O密集型场景。它不是让单次请求变快而是让系统在等待I/O的过程中不浪费线程资源。但这里有个前提需要说清楚WebFlux不是银弹。如果你的接口是CPU密集型的比如大量计算、图像处理、复杂算法那WebFlux帮不了你甚至可能因为响应式编程的额外开销而略微变慢。它真正擅长的是高并发、I/O等待时间占比高的场景——网关、聚合层、实时推送、流式数据处理这些才是WebFlux的主场。还有一个常见的误解有人觉得用了WebFlux就一定要把整个项目全部改成响应式。实际上Spring Boot允许Servlet和Reactive共存你完全可以在一个项目里让大部分接口继续用Spring MVC只把少数几个高并发的聚合接口用WebFlux来实现。这种渐进式的策略在实际项目中往往更务实。这篇文章是WebFlux系列的上篇重点放在核心概念、线程模型、与Spring MVC的本质差异以及第一个可运行的WebFlux项目搭建。下篇会深入讲背压、WebClient、SSE、WebSocket以及和数据库的响应式集成。如果你正在考虑要不要在项目里引入WebFlux或者已经用了但总觉得没吃透那这篇内容应该能帮你把底层逻辑理清楚。2. 响应式流规范与Reactor的核心角色2.1 响应式流到底规定了什么在聊Reactor之前得先搞清楚它背后的规范——Reactive Streams。这个规范其实非常简短核心就是四个接口Publisher、Subscriber、Subscription、Processor。它解决的问题只有一个在异步环境下如何让生产者根据消费者的能力来发送数据而不是无限制地推送导致消费者被压垮。这个机制叫背压Backpressure。举个生活中的例子你家的水龙头一直开着但下水道排水能力有限如果没有一个机制告诉水龙头“你慢点放水”那水槽迟早会溢出来。响应式流规范就是定义了一套“下水道告诉水龙头该放多少水”的标准协议。规范里定义了Publisher的subscribe方法Subscriber收到数据前会先收到一个Subscription对象通过它来请求指定数量的数据或者取消订阅。这套机制看起来简单但它是整个响应式编程生态的基石。Reactor、RxJava、Akka Streams都实现了这套规范所以它们之间可以互操作。2.2 Reactor的两种核心类型Mono与FluxReactor是Spring WebFlux默认的响应式库它提供了两个核心类型Mono表示0个或1个元素的异步序列。可以理解为“未来某个时刻会有一个值或者没有值”。Flux表示0个到N个元素的异步序列。可以理解为“未来某个时刻会有一串值陆续到达”。这两个类型都实现了Publisher接口。你可能会问为什么不用Java自带的CompletableFuture因为CompletableFuture只能表示单个结果而且它没有背压机制。Flux可以表示流式数据比如从数据库里逐条读取一百万条记录消费者可以按自己的节奏来请求数据。在实际使用中Mono和Flux提供了大量的操作符比如map、flatMap、filter、zip、merge、concat等等。这些操作符让你可以用声明式的方式组合异步逻辑而不需要写回调地狱。2.3 冷序列与热序列的区别这是很多初学者容易忽略的一个概念。冷序列是指每次订阅都会重新开始产生数据比如一个从数据库查询的Flux每个订阅者都会触发一次新的查询。热序列是指数据在订阅之前就已经开始产生后来的订阅者只能收到订阅之后的数据比如一个WebSocket连接或者一个事件总线。大部分Reactor的操作符返回的都是冷序列。这个特性在实际开发中很重要因为如果你不小心对一个冷序列订阅了两次那底层的查询或请求就会执行两次。我见过有同学在代码里写了这样的逻辑先调一次Mono获取结果做判断然后又订阅一次做返回结果下游服务被调了两次。这种问题在同步代码里不会出现但在响应式代码里如果不注意就会踩坑。解决方式是用cache()操作符把结果缓存起来或者用share()把冷序列转成热序列。但要注意cache的缓存策略如果数据会变化缓存时间需要仔细设计。3. WebFlux的线程模型事件循环到底怎么跑3.1 与Spring MVC线程模型的对比Spring MVC基于Servlet API默认使用Tomcat的线程池。每个请求进来Tomcat从线程池里分配一个线程这个线程从头到尾处理这个请求——包括参数解析、业务逻辑、数据库查询、远程调用、结果序列化。如果中间有阻塞操作比如等待数据库返回这个线程就干等着什么也做不了。默认情况下Tomcat的最大线程数是200。这意味着同时只能有200个请求被处理第201个请求就要排队。如果你的接口平均响应时间是500ms那理论上每秒最多处理400个请求200线程 / 0.5秒。想提高吞吐量要么加线程数但线程多了上下文切换开销大要么缩短响应时间。WebFlux的模型完全不同。它默认使用Netty作为服务器也可以换成Undertow或Servlet 3.1容器底层是事件循环Event Loop模型。Netty的EventLoop数量默认是CPU核数乘以2。每个EventLoop是一个单线程的循环不断检查是否有I/O事件就绪就绪就处理没就绪就继续轮询。关键在于当WebFlux需要执行阻塞操作时它不会让EventLoop线程去等而是把任务提交到另一个线程池然后立即释放EventLoop线程去处理其他请求。等阻塞操作完成后再回到EventLoop线程继续处理响应。这样少量的EventLoop线程就能支撑大量的并发连接。3.2 默认线程池的配置与调度器Reactor提供了几种调度器SchedulerSchedulers.immediate()在当前线程执行不切换。Schedulers.single()使用一个可复用的单线程。Schedulers.parallel()使用固定大小的线程池大小默认等于CPU核数。Schedulers.boundedElastic()使用一个有界弹性线程池适合执行阻塞任务。在WebFlux中Controller的方法默认在EventLoop线程上执行。如果你在方法里做了阻塞操作比如用JDBC查数据库那EventLoop线程就被卡住了整个事件循环都会受影响。这时候需要用subscribeOn(Schedulers.boundedElastic())把阻塞操作切换到弹性线程池。但更好的做法是根本不要在WebFlux的链路上使用阻塞API。用R2DBC代替JDBC用WebClient代替RestTemplate用响应式的Redis客户端代替传统的Jedis。如果实在避不开阻塞调用比如某个老系统的SDK只有同步版本那就必须显式地切换到boundedElastic调度器。3.3 一个容易踩的坑在EventLoop里做阻塞操作我见过一个很典型的案例某同学在WebFlux的Controller里直接调用了RestTemplate去请求下游服务。测试环境没问题因为并发低。上线后并发一上来整个服务响应变得极慢甚至出现大量超时。原因就是RestTemplate是阻塞的它把EventLoop线程占住了导致事件循环无法处理其他请求。排查这个问题的过程也很有意思。当时看监控发现CPU使用率并不高但请求延迟飙升。用jstack看线程栈发现大量EventLoop线程都卡在socketRead上。换成WebClient之后问题立刻消失。所以记住一条铁律在WebFlux的请求处理链路上任何阻塞调用都必须被隔离到boundedElastic调度器上或者干脆换成非阻塞的客户端。4. 搭建第一个WebFlux项目从依赖到跑通4.1 依赖选择与项目结构用Spring Initializr创建项目时选择Spring Reactive Web即可。核心依赖是spring-boot-starter-webflux它包含了Reactor、Netty、WebFlux的核心模块。如果你同时需要Spring MVC和WebFlux可以两个starter都加但要注意默认情况下Spring Boot会优先使用WebFlux因为Netty在classpath上。一个典型的pom依赖配置如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency如果你需要响应式数据库访问可以加dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-r2dbc/artifactId /dependency项目结构跟普通的Spring Boot项目没有区别Controller、Service、Repository分层照旧只是返回类型从普通的对象变成了Mono或Flux。4.2 编写第一个响应式Controller一个最简单的例子RestController RequestMapping(/api) public class HelloController { GetMapping(/hello) public MonoString hello() { return Mono.just(Hello WebFlux); } GetMapping(/stream) public FluxString stream() { return Flux.interval(Duration.ofSeconds(1)) .map(i - tick- i) .take(5); } }第一个接口返回单个值第二个接口每秒返回一个值共返回5个。用浏览器访问/api/stream你会看到内容逐条出现而不是一次性返回。这就是流式响应的效果。需要注意的是Flux.interval默认运行在parallel调度器上。如果你在interval后面直接做阻塞操作同样会卡住EventLoop。所以链路上的每个操作符都要注意是否引入了阻塞。4.3 用WebTestClient做测试WebFlux提供了一个专门的测试客户端WebTestClient它可以很方便地测试响应式接口SpringBootTest AutoConfigureWebTestClient class HelloControllerTest { Autowired private WebTestClient webTestClient; Test void testHello() { webTestClient.get().uri(/api/hello) .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo(Hello WebFlux); } Test void testStream() { webTestClient.get().uri(/api/stream) .exchange() .expectStatus().isOk() .returnResult(String.class) .getResponseBody() .take(3) .collectList() .block() .forEach(System.out::println); } }WebTestClient的好处是它本身就是响应式的可以测试流式接口而且不需要启动真实的服务器也可以选择启动。5. 响应式编程中那些反直觉的地方5.1 没有订阅什么都不会发生这是响应式编程最核心也最容易忘的一点。你写了一个Mono做了一堆map、flatMap但如果最后没有subscribe整个链路不会执行。这跟Stream API有点像但Stream是同步的你忘了终端操作编译器会提醒你。而Mono/Flux作为返回值交给WebFlux框架时框架会自动订阅所以Controller里不写subscribe没问题。但如果你在Service里手动创建了一个Mono做副作用操作忘了订阅那它永远不会执行。我见过有同学在Service里写了这样的代码public void doSomething() { Mono.just(data) .flatMap(this::process) .doOnNext(result - log.info(done: {}, result)); // 没有subscribe什么都不会发生 }这种代码不会报错但也不会执行。正确的做法是返回Mono让调用方订阅或者在确实需要触发副作用时显式subscribe。5.2 flatMap和concatMap的选择flatMap和concatMap都是把内层Publisher的元素展平但行为不同。flatMap是并发执行的内层Publisher的顺序不确定concatMap是串行执行的保证顺序。举个例子你有一个Flux 需要对每个元素调用一个远程接口。用flatMap的话所有调用会同时发起速度快但结果顺序可能乱用concatMap的话一个调完再调下一个顺序有保证但速度慢。选择哪个取决于业务需求。如果顺序不重要用flatMap提高吞吐如果顺序重要比如写数据库有依赖关系用concatMap。还有一个折中方案是flatMapSequential它并发执行但按原始顺序收集结果。5.3 错误处理的正确姿势在响应式链路里错误处理跟在同步代码里完全不同。try-catch只能捕获同步抛出的异常异步链路里的错误需要通过onErrorResume、onErrorReturn、doOnError等操作符来处理。public MonoString fetchData() { return webClient.get().uri(/data) .retrieve() .bodyToMono(String.class) .onErrorResume(e - { log.error(fetch failed, e); return Mono.just(default); }); }onErrorResume可以返回一个降级值也可以返回另一个Mono继续链路。onErrorReturn只能返回一个固定值。doOnError只做副作用比如打日志不会消费错误错误还会继续往下传。还有一个容易忽略的点响应式链路里的错误如果没有被处理最终会以onError信号传递给订阅者。在WebFlux中框架会把它转换成HTTP 500响应。但如果你在链路中间用了onErrorResume吞掉了错误那上层就感知不到了。所以错误处理的位置和策略需要仔细设计。6. 什么时候该用WebFlux什么时候不该用6.1 适合的场景API网关需要转发大量请求每个请求的I/O等待时间长WebFlux可以用少量线程支撑高并发。聚合服务一个接口需要调用多个下游服务WebFlux可以并发发起调用减少总响应时间。实时推送SSE、WebSocket等长连接场景WebFlux的流式模型天然适合。流式数据处理从消息队列或数据库读取大量数据逐条处理并返回。6.2 不适合的场景CPU密集型任务大量计算、加密解密、图像处理WebFlux没有优势。团队不熟悉响应式编程响应式代码的调试和排错门槛比同步代码高如果团队没有经验强行上WebFlux可能适得其反。已有大量阻塞依赖如果项目中大量使用了JDBC、RestTemplate、同步的Redis客户端改成响应式的成本很高收益可能不明显。6.3 渐进式迁移的策略如果决定引入WebFlux不建议一次性全量迁移。可以先从一两个高并发的聚合接口开始用WebFlux实现其他接口继续用Spring MVC。Spring Boot支持两种栈共存只要注意不要在同一条链路上混用阻塞和非阻塞即可。等团队对响应式编程熟悉了再逐步扩大范围。数据库访问可以先继续用JDBC通过boundedElastic隔离等R2DBC生态更成熟后再迁移。7. 上篇小结与下篇预告写到这里WebFlux的核心概念和第一个可运行的项目已经讲完了。回顾一下重点WebFlux解决的是高并发下I/O等待浪费线程的问题它的核心是响应式流规范和Reactor的Mono/Flux类型线程模型基于事件循环使用时必须避免在EventLoop上做阻塞操作。我个人在实际项目中的体会是WebFlux的学习曲线主要不在API本身而在于思维方式的转变——从“命令式地一步步执行”变成“声明式地组合异步流”。刚开始写的时候总觉得别扭但写多了之后会发现很多以前需要回调或者CompletableFuture嵌套的场景用Reactor的操作符表达起来反而更清晰。下篇会重点讲WebClient的实战用法、背压的具体处理策略、SSE和WebSocket的实现以及R2DBC的集成。如果你在搭建过程中遇到了什么问题或者对某个概念还有疑问可以先检查一下是不是在链路上混入了阻塞调用——这是新手最容易踩的坑没有之一。

相关新闻

开题报告怎么写?AI学术写作工具助你高效生成规范初稿

开题报告怎么写?AI学术写作工具助你高效生成规范初稿

1. 开题报告这项苦差事,到底难在哪先说个真实经历。我读研那会儿,导师布置开题报告的时间是国庆前,要求假期结束提交初稿。我在老家待了七天,有六天半都在跟文献综述搏斗:知网刷了几百篇论文,下载了四十多篇…

2026/10/12 8:33:07 阅读更多 →
SpringBoot+Vue企业人事管理系统设计与实现

SpringBoot+Vue企业人事管理系统设计与实现

基于SpringBootVue的企业人事管理系统LW参考示例在企业级开发这个圈子里,SpringBootVue基本已经是前后端分离项目的标准答案了。这套企业人事管理系统,是我在带过好几批学生、也自己从头到尾重构过几遍之后,沉淀出来的一套最稳妥的落地方案。…

2026/10/12 8:33:07 阅读更多 →
告别 AI 失忆:用 claude-mem 给 Claude Code 加跨会话记忆层

告别 AI 失忆:用 claude-mem 给 Claude Code 加跨会话记忆层

1. 被上下文窗口碾过之后,我给 Claude Code 加了个记忆层 先说说我为什么会折腾 claude-mem 这个工具。 用 Claude Code 干活的头一个月,我最深的感受不是"AI 真能写代码",而是"这工具怎么这么健忘"。同一个项目&#x…

2026/10/12 8:32:06 阅读更多 →

最新新闻

数据复制深度拆解:从HDFS块复制到CDC同步的机制、实践与容灾设计

数据复制深度拆解:从HDFS块复制到CDC同步的机制、实践与容灾设计

大数据领域做久了,你会发现一个很朴素但又绕不开的现实: 数据复制 几乎贯穿了所有核心系统的生命周期。无论是离线数仓里的表同步、实时链路里的CDC采集,还是跨机房容灾里的镜像复制,本质上都在做同一件事——把一份数据从A点搬…

2026/10/12 9:23:59 阅读更多 →
开源大模型权重质变:推理优化、量化与微调工程实践指南

开源大模型权重质变:推理优化、量化与微调工程实践指南

1. 从"权重开放"到"能力平权":一场静默的行业拐点 过去两年,我一直在跟踪各类开源模型权重发布后的实际落地情况。说实话,早期大部分所谓"开放权重"的模型,更多是实验室里的展示品——参数量大、推…

2026/10/12 9:23:59 阅读更多 →
配电网多目标Pareto重构:Matlab实现NSGA-II的实战经验与代码解析

配电网多目标Pareto重构:Matlab实现NSGA-II的实战经验与代码解析

配电网多目标Pareto重构,核心就一句话:通过改变分段开关和联络开关的开合状态,重新编排配电网的供电路径,让网损、电压质量、开关操作成本等多个目标同时尽可能好。这个问题我前后做了一段时间,用Matlab实现了多套智能…

2026/10/12 9:23:59 阅读更多 →
把手机变成AI服务器:openclaw-termux SSH远程访问配置实战教程

把手机变成AI服务器:openclaw-termux SSH远程访问配置实战教程

AI 应用LLM 网关大模型移动开发 【免费下载链接】openclaw-termux Run OpenClaw AI Gateway on Android — standalone Flutter app with built-in terminal, web dashboard, and one-tap setup. Also available as a Termux CLI package. 项目地址: https://gitcod…

2026/10/12 9:23:58 阅读更多 →
自己动手实现TreeMultiSet:基于TreeMap的有序可重复集合

自己动手实现TreeMultiSet:基于TreeMap的有序可重复集合

1. 从一个真实的数据统计需求说起去年帮一个做电商的朋友处理订单数据时&#xff0c;遇到一个挺典型的问题&#xff1a;他们需要统计每个商品在一天内的被下单次数&#xff0c;并且要按商品ID排序输出。第一反应是用TreeMap<Long, Integer>&#xff0c;key 是商品ID&…

2026/10/12 9:22:58 阅读更多 →
PS5折腾指南:系统配置、存储扩展、远程游玩与散热维护

PS5折腾指南:系统配置、存储扩展、远程游玩与散热维护

PS5到手三个月后&#xff0c;我发现自己陷入了一个尴尬循环&#xff1a;开机看一眼、随便通关一个进度、然后切出去看手机&#xff0c;主机外壳摸起来都能煎鸡蛋。直到我给自己定了个"AnyPS5"计划——不把这台机器当普通游戏机&#xff0c;而是当成一台可折腾的娱乐终…

2026/10/12 9:22:58 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天&#xff0c;画面可以做到绝对的锐利、平滑与无瑕。然而&#xff0c;当一张秋日手账插画或拍立得照片过于“平整无瑕”时&#xff0c;往往会散发出一种冰冷生硬的“数码塑料感&#xff08;Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中&#xff0c;横排&#xff08;Horizontal Layout&#xff09;早已经成为了绝对的主流。然而&#xff0c;当我们翻开泛黄的线装古籍、宋版木刻诗集&#xff0c;或是欣赏一张茶道雅集的手写便签时&#xff0c;那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点&#xff0c;很多人心里都会悄悄亮起一盏警示灯。 在心理学上&#xff0c;这种现象有一个专门的称谓——“周日夜晚焦虑症&#xff08;Sunday Scaries&#xff09;”。明天又是周一&#xff0c;闹钟又要重新在七点响彻卧房&#xff1b;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介&#xff1a;基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码&#xff0c;面向计算机相关专业课程设计与期末大作业学生&#xff0c;以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程&#xff0c;…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程&#xff1a;键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化&#xff0c;十个新手有八个栽在"往输入框里填东西"这件事上&#xff1a;要么填不进去&#xff0c;要么填了一半&#xff0c;要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程&#xff1a;阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀&#xff1a;什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面&#xff0c;跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →