一、前置核心概念(面试必问开篇)
1.1 传统阻塞编程痛点(SpringMVC)
传统 SpringMVC:一请求一线程(Tomcat 线程池)
- Tomcat 默认线程池核心数:10-200 有限固定线程
- 线程一旦发起 DB 查询、RPC 调用、网络等待,线程全程阻塞休眠,CPU 空闲
- 高并发场景:大量请求堆积等待线程,吞吐量急剧下降,容易线程耗尽雪崩
1.2 响应式编程定义
响应式编程(Reactive Programming):面向数据流、异步非阻塞、事件驱动 核心特质:
- 异步非阻塞:等待 IO 时释放线程,线程复用处理其他请求
- 数据流:数据源源不断推送(0 个 / 1 个 / 多个元素)
- 背压(Backpressure):下游消费慢时,上游主动限流,防止内存溢出
- 事件驱动:数据到达、异常、完成都会触发回调事件
1.3 两组关键区分
同步 VS 异步
- 同步:调用方等待结果返回,线程阻塞
- 异步:调用发起后立刻返回,结果通过回调 / 订阅通知
阻塞 VS 非阻塞
- 阻塞:IO 等待期间线程挂起,无法干活
- 非阻塞:IO 等待时线程释放,去处理别的任务,IO 就绪后再回调处理
1.4 Reactive Streams 官方规范(业界统一标准)
Java 响应式统一规范,解决各框架不兼容问题,4 大核心接口:
plaintext
1. Publisher 发布者:产生数据流(上游)
void subscribe(Subscriber subscriber);
2. Subscriber 订阅者:消费数据流(下游)
void onSubscribe(Subscription s); //订阅建立
void onNext(T t); //收到数据
void onError(Throwable t); //异常
void onComplete(); //数据流结束
3. Subscription 订阅令牌:背压控制核心
void request(long n); //向下游申请n条数据
void cancel(); //取消订阅
4. Processor:既是Publisher又是Subscriber,数据流中间处理节点
背压核心 :下游通过request(n)告诉上游我能承受多少数据,上游不会疯狂推送压垮下游。
二、SpringBoot 响应式技术栈整体架构(图文结构)
整体架构层级(文本架构图)
plaintext
客户端 <-----> 非阻塞容器(Netty)
↓
Spring WebFlux(响应式Web框架,替代SpringMVC)
↓
Reactor(Spring官方默认响应式实现框架,实现Reactive Streams)
↓
底层:NIO + 事件循环 + 线程调度器(Scheduler)
配套组件:
响应式DB:Spring Data R2DBC / Reactive MongoDB / Redis Reactive
响应式RPC:Spring Cloud Gateway、WebClient
两大核心容器区别
- SpringMVC:容器 Tomcat(Servlet 阻塞容器,基于 BIO/NIO 阻塞模式)
- WebFlux:默认容器 Netty(异步非阻塞 Reactor 模式),也可适配 Jetty、Undertow
三、Reactor 核心:Mono & Flux(面试重中之重)
Spring WebFlux 底层封装 Reactor,只有两个数据流类型:
3.1 Flux
- 含义:0~N 个元素的数据流(集合、列表、多条数据)
- 终止事件:onComplete /onError
3.2 Mono
- 含义:0 或 1 个元素的数据流(单个对象、空、无返回)
- 场景:单条查询、新增修改返回结果
3. 基础创建代码示例
java
运行
//1. Flux 创建多条数据
Flux<String> flux = Flux.just("张三","李四","王五");
//数组、集合创建
Flux<Integer> numFlux = Flux.fromArray(new Integer[]{1,2,3});
//2. Mono 创建单个数据/空数据
Mono<String> mono = Mono.just("单个用户");
Mono<Void> empty = Mono.empty(); //无数据
Mono<String> error = Mono.error(new RuntimeException("异常"));
//3. 订阅消费(响应式必须订阅才会执行,惰性加载)
flux.subscribe(
data -> System.out.println("收到数据:"+data), //onNext
err -> err.printStackTrace(), //onError
() -> System.out.println("数据流结束") //onComplete
);
4. 常用操作符(面试常考)
表格
| 操作符 | 作用 |
|---|---|
| map | 同步转换元素 |
| flatMap | 异步流转(嵌套 Mono/Flux,最常用) |
| filter | 过滤数据 |
| concat/merge | 合并数据流 |
| zip | 多个数据流配对合并 |
| timeout | 设置超时 |
| retry | 异常重试 |
| doOnNext/doOnError | 回调埋点(日志、监控) |
四、线程调度 Scheduler(底层原理必考题)
Reactor 通过 Scheduler 管控线程池,区分不同执行场景:
- Schedulers.single():单一线程,顺序执行
- Schedulers.parallel():CPU 密集型线程池(默认 CPU 核心数)
- Schedulers.boundedElastic():IO 密集型线程池(数据库、网络 IO 首选,弹性扩容)
- Schedulers.immediate():当前线程直接执行
切换线程两个核心方法:
publishOn():下游线程切换(后续操作运行在指定线程)subscribeOn():上游源头线程切换(整个数据流源头运行线程)
面试考点:两者区别,subscribeOn 全局生效,publishOn 就近生效
五、Spring WebFlux 完整架构图解
5.1 WebFlux 两种编程模式
plaintext
WebFlux
├─ 注解模式(和SpringMVC写法几乎一致,上手简单)
└─ 函数式编程模式(RouterFunction + HandlerFunction,纯流式定义路由,更灵活)
5.2 注解模式实战代码(日常开发主流)
1)启动依赖 pom.xml
xml
<!-- 引入webflux,排除springmvc-tomcat依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2)响应式 Controller
java
运行
@RestController
@RequestMapping("/user")
public class UserReactiveController {
// 返回单个用户 Mono
@GetMapping("/{id}")
public Mono<User> getUserById(@PathVariable Long id){
// 模拟响应式数据库查询
return userReactiveService.getById(id);
}
// 返回用户列表 Flux
@GetMapping("/list")
public Flux<User> getUserList(){
return userReactiveService.listAll();
}
// 新增
@PostMapping("/add")
public Mono<User> addUser(@RequestBody Mono<User> userMono){
return userMono.flatMap(user -> userReactiveService.save(user));
}
}
5.3 函数式编程模式(面试偶尔考察)
路由 + 处理器拆分,无注解:
java
运行
// 处理器
@Component
public class UserHandler {
public Mono<ServerResponse> getUser(ServerRequest request){
Long id = Long.valueOf(request.pathVariable("id"));
Mono<User> userMono = userService.getById(id);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(userMono, User.class);
}
}
// 路由注册
@Configuration
public class RouterConfig {
@Bean
public RouterFunction<ServerResponse> userRouter(UserHandler handler){
return RouterFunctions.route()
.GET("/user/{id}", handler::getUser)
.build();
}
}
六、WebFlux 底层执行流程(口述结构图)
客户端请求 → Netty NIO 事件轮询器 (EventLoop) 接收连接 → 请求封装成 ServerWebExchange(上下文,替代 SpringMVC 的 HttpServletRequest) → 路由匹配处理器 Controller/HandlerFunction → 执行业务逻辑(DB/RPC 非阻塞调用,线程释放) → IO 就绪后回调线程推送 Mono/Flux 数据流 → Netty 异步写回响应给客户端
核心:全程不会长时间占用固定工作线程
七、SpringMVC VS WebFlux 面试高频对比表
表格
| 对比维度 | SpringMVC(阻塞) | Spring WebFlux(响应式非阻塞) |
|---|---|---|
| 底层容器 | Tomcat、Jetty(Servlet 阻塞规范) | Netty(NIO 异步事件驱动) |
| 线程模型 | 一请求一线程,线程池固定上限 | 少量核心线程,IO 等待释放线程,高并发吞吐高 |
| 编程模型 | 同步阻塞,返回实体类 | 异步流式,返回 Mono/Flux |
| 数据库支持 | JDBC 阻塞 | R2DBC 响应式驱动 |
| 适用场景 | 业务复杂、CPU 计算多、并发一般 | 高并发 IO 密集:网关、推送、大量接口调用 |
| 学习成本 | 低 | 较高,需要理解流式、背压、异步回调 |
| 事务支持 | 完善 | 有限,R2DBC 事务复杂度更高 |
面试官追问标准答案:
不是 WebFlux 性能一定碾压 MVC:CPU 密集型业务 WebFlux 没有优势;海量 IO 等待场景(网关、千万级并发接口)WebFlux 吞吐量优势巨大。
八、配套响应式中间件实战
8.1 响应式数据库 R2DBC(替代 JDBC)
JDBC 是阻塞 API,无法适配响应式,WebFlux 必须使用 R2DBC 非阻塞驱动 核心依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
Repository 写法:
java
运行
public interface UserR2dbcRepository extends R2dbcRepository<User,Long> {
// 返回Mono/Flux
Mono<User> findById(Long id);
Flux<User> findAll();
}
8.2 WebClient(响应式 HTTP 调用,替代 RestTemplate)
RestTemplate 阻塞,WebFlux 内部推荐 WebClient 异步调用第三方接口
java
运行
// 构建客户端
WebClient webClient = WebClient.create("https://xxx.api.com");
// 异步调用
Mono<String> resultMono = webClient.get()
.uri("/info")
.retrieve()
.bodyToMono(String.class);
九、大厂高频面试题详细解析(含答题话术、踩坑点)
面试题 1:什么是响应式编程?解决了什么问题?
标准答案回答: 响应式编程是一套基于异步非阻塞、事件驱动、数据流的编程范式,遵循 Reactive Streams 规范,核心引入背压机制。 传统阻塞编程中线程遇到 IO 会被挂起,CPU 资源浪费,并发上限受线程池限制;响应式在线程等待 IO 时释放工作线程,线程可以复用处理其他请求,大幅提升 IO 密集场景吞吐量;同时背压可以解决上下游数据速率不一致导致的内存溢出问题。
面试题 2:Mono 和 Flux 区别?各自使用场景?
- Flux:承载 0~N 个元素数据流,多用于列表查询、批量数据推送;
- Mono:承载 0 或 1 个元素,多用于单条查询、新增修改、无返回接口; 底层两者都实现 Publisher 接口,Mono 可以转 Flux,Flux 也可聚合为 Mono。
面试题 3:publishOn 和 subscribeOn 区别?
- subscribeOn:修改数据源上游的执行线程,无论放在代码哪个位置,都会影响整个数据流源头线程,全局生效;
- publishOn:修改后续下游代码的执行线程,只对该方法之后的操作生效,就近生效; IO 密集场景一般用 boundedElastic 线程池。
面试题 4:背压(Backpressure)是什么?WebFlux 如何实现?
- 定义:上游生产数据速度远大于下游消费速度,若无限推送会堆积队列、内存溢出,背压就是下游向上游反馈自身消费能力,上游按需推送数据的限流机制;
- 实现:基于 Reactive Streams 的 Subscription 接口,下游调用 request (n) 申请指定条数数据,上游严格按照申请量推送。
面试题 5:SpringMVC 和 WebFlux 该如何技术选型?
答题分层:
- 选用 SpringMVC:
- 业务逻辑复杂、大量 CPU 运算、事务依赖重
- 团队没有响应式技术储备、迭代效率优先
- 并发量不高,传统架构完全够用
- 选用 WebFlux:
- IO 密集型场景:微服务网关(Spring Cloud Gateway 基于 WebFlux)、海量并发接口、消息推送、爬虫聚合
- 需要极致吞吐,服务器资源有限需要压榨性能 补充:CPU 密集型 WebFlux 不会提升性能,反而因为异步调度有微小损耗。
面试题 6:WebFlux 有什么缺点?
- 调试难度大:异步流式代码栈追踪困难,日志排查问题繁琐;
- 生态适配有限:传统 JDBC、大部分老工具都是阻塞 API,需要替换 R2DBC、Redis Reactive;
- 事务管控复杂:R2DBC 事务相较于 JDBC 事务易用性差;
- 学习门槛高:需要理解异步、回调、线程调度、流式操作符,新手容易写出阻塞 bug;
- 第三方阻塞代码一旦嵌入 WebFlux,会直接破坏非阻塞模型,性能退化。
面试题 7:WebFlux 中不小心调用了阻塞代码会怎么样?怎么解决?
问题:阻塞代码会占用 Netty 核心 EventLoop 线程,事件循环被卡住,所有请求处理停滞,性能暴跌。 解决方案:
- 将阻塞代码丢入
Schedulers.boundedElastic()线程池执行,隔离阻塞操作;
java
运行
Mono.fromCallable(() -> 阻塞业务方法())
.subscribeOn(Schedulers.boundedElastic());
- 尽量替换为原生响应式非阻塞 SDK。
面试题 8:Spring Cloud Gateway 为什么基于 WebFlux 而不是 SpringMVC?
网关核心诉求:海量请求转发、大量网络 IO 等待、高吞吐、低资源占用
- MVC 固定线程池无法支撑网关高并发转发场景,线程极易打满;
- WebFlux 非阻塞模型,少量线程即可处理上万并发连接,内存占用更低;
- 内置 WebClient 异步转发请求,适配上下游异步调用场景。
面试题 9:Flux 的 flatMap 和 concatMap 区别?
- flatMap:异步并发处理元素,返回数据顺序无序,吞吐更高;
- concatMap:串行有序处理,严格按照上游数据顺序返回,性能偏低; 业务有序需求选 concatMap,追求并发吞吐选 flatMap。
面试题 10:WebFlux 异常处理三种方式?
- 操作符内置:
onErrorReturn()异常返回默认值、onErrorResume()异常降级返回数据流、retry()重试; - 全局统一异常处理器:@ControllerAdvice + @ExceptionHandler(注解模式通用);
- 函数式模式:onError 回调统一捕获。
十、面试口述加分总结(收尾话术)
响应式编程核心价值是优化 IO 密集型并发吞吐,Spring WebFlux 基于 Reactor+Netty 实现非阻塞异步 Web 服务,依靠 Mono/Flux 流式编程、Scheduler 线程调度、背压机制解决传统 Servlet 阻塞线程瓶颈;工作中不会全盘替换 MVC,一般网关、高并发接口使用 WebFlux,常规业务系统依旧使用 SpringMVC 做平衡选型,同时开发中严格规避阻塞代码保证非阻塞特性。
十一、面试避雷坑
- 不要说 WebFlux 性能全面碾压 MVC,区分 IO/CPU 场景;
- 牢记 JDBC 不能用于 WebFlux 响应式场景,必须 R2DBC;
- 区分 publishOn/subscribeOn 作用范围;
- 背压是 Reactive Streams 核心,不能遗忘。
