一、开篇故事:餐厅服务员的两种工作模式 🍽️
传统阻塞式(Spring MVC)------ 专职服务员
想象一家传统餐厅,每个服务员负责一桌客人:
- 服务员A走到1号桌接单 ✍️
- 把订单送到厨房,然后在厨房门口等着(阻塞)😴
- 菜做好了,端回1号桌
- 才能去服务下一桌
问题: 服务员大部分时间都在等待!厨房做菜时(IO操作),服务员傻站着,浪费人力!
响应式(WebFlux)------ 灵活服务员
响应式餐厅的服务员聪明多了:
- 服务员走到1号桌接单 ✍️
- 把订单送到厨房,马上去服务2号桌(非阻塞)💨
- 接2号桌订单,送到厨房,再去服务3号桌
- 厨房通知:"1号桌的菜好了!"📢
- 服务员端菜给1号桌,然后继续服务其他桌
优势: 一个服务员可以同时服务多桌客人,效率爆表!💪
二、什么是响应式编程?🤔
2.1 核心概念
响应式编程(Reactive Programming) 是一种基于数据流 和变化传播的编程范式。
关键特点:
- 🌊 异步非阻塞:不等待,立即返回
- 📡 数据流驱动:数据像水流一样流动
- 🔙 背压(Backpressure):下游可以控制上游的速度
- 🎯 事件驱动:有事件才处理,没事件就休息
2.2 生活类比
传统编程:
arduino
你: "喂,快递到了吗?"
快递: "还没,等一下..."
你: (站在门口傻等30分钟)😴
快递: "到了!"
你: "好的,我来拿。"
响应式编程:
arduino
你: "快递到了通知我。" (留下电话号码)
你: (去做其他事情:工作、吃饭、打游戏)🎮
快递: (30分钟后)"叮咚!快递到了!" 📲
你: "好的,我来拿。"
三、WebFlux核心组件 🎯
3.1 Mono ------ 单个数据流
Mono 表示0个或1个元素的异步序列。
java
// 类比:一个信封,里面有0封或1封信
Mono<String> mono = Mono.just("Hello WebFlux"); // 有1个元素
Mono<String> empty = Mono.empty(); // 没有元素
Mono<String> error = Mono.error(new RuntimeException()); // 错误
生活例子:
- 查询用户信息(找到1个或没找到)
- 发送一封邮件(成功或失败)
- 调用一个API(返回1个结果)
3.2 Flux ------ 多个数据流
Flux 表示0个到N个元素的异步序列。
java
// 类比:一个信箱,里面有很多封信
Flux<Integer> flux = Flux.just(1, 2, 3, 4, 5); // 有5个元素
Flux<Integer> range = Flux.range(1, 100); // 1到100
Flux<String> empty = Flux.empty(); // 没有元素
生活例子:
- 查询商品列表(可能有很多商品)
- 实时股票价格(持续流动的数据)
- 日志流(不断产生的日志)
3.3 图解Mono vs Flux
ini
Mono(单个):
[数据] ---> 下游
Flux(多个):
[数据1] --->
[数据2] --->
[数据3] --->
[数据4] ---> 下游
四、WebFlux vs Spring MVC 对决 ⚔️
4.1 处理模型对比
Spring MVC(阻塞式)
java
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
// 1. 阻塞等待数据库查询(假设100ms)
User user = userService.findById(id);
// 2. 阻塞等待外部API调用(假设200ms)
user.setAvatar(avatarService.getAvatar(user.getId()));
// 3. 总耗时:100 + 200 = 300ms
// 这个线程在300ms内都被占用!
return user;
}
}
资源使用:
- 每个请求占用1个线程
- 1000个并发 = 需要1000个线程
- 线程切换开销大
- 内存占用高
WebFlux(非阻塞式)
java
@RestController
public class UserController {
@Autowired
private UserReactiveService userService;
@GetMapping("/user/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userService.findById(id) // 非阻塞查询
.flatMap(user ->
avatarService.getAvatar(user.getId()) // 非阻塞API调用
.map(avatar -> {
user.setAvatar(avatar);
return user;
})
);
// 立即返回Mono,不阻塞线程!
// 数据准备好后自动响应给客户端
}
}
资源使用:
- 少量线程(通常等于CPU核心数)
- 1000个并发 = 只需要几个到几十个线程
- 极低的线程切换开销
- 内存占用低
4.2 对比表
| 特性 | Spring MVC | Spring WebFlux |
|---|---|---|
| 编程模型 | 阻塞式 | 响应式 |
| 底层技术 | Servlet(Tomcat) | Netty / Reactor |
| 线程模型 | 一请求一线程 | 少量线程处理大量请求 |
| 适用场景 | CPU密集型 | IO密集型 |
| 学习曲线 | 平缓 ⭐⭐ | 陡峭 ⭐⭐⭐⭐⭐ |
| 生态成熟度 | 非常成熟 | 逐渐成熟 |
| 并发能力 | 中等(受限于线程数) | 高(非阻塞) |
五、WebFlux核心操作符 🛠️
5.1 创建操作符
java
// 1. 从单个值创建
Mono<String> mono = Mono.just("Hello");
// 2. 从多个值创建
Flux<Integer> flux = Flux.just(1, 2, 3, 4, 5);
// 3. 从范围创建
Flux<Integer> range = Flux.range(1, 100); // 1到100
// 4. 从集合创建
List<String> list = Arrays.asList("A", "B", "C");
Flux<String> fromList = Flux.fromIterable(list);
// 5. 空流
Mono<String> empty = Mono.empty();
Flux<String> emptyFlux = Flux.empty();
// 6. 延迟创建
Mono<String> defer = Mono.defer(() -> {
// 订阅时才执行
return Mono.just("Delayed");
});
// 7. 定时创建
Flux<Long> interval = Flux.interval(Duration.ofSeconds(1)); // 每秒发射一个数字
5.2 转换操作符
java
// 1. map - 一对一转换
Flux<String> flux = Flux.just("apple", "banana", "cherry")
.map(String::toUpperCase); // ["APPLE", "BANANA", "CHERRY"]
// 2. flatMap - 一对多转换(拍平)
Flux<String> words = Flux.just("Hello World", "Reactive Programming")
.flatMap(sentence ->
Flux.fromArray(sentence.split(" "))
); // ["Hello", "World", "Reactive", "Programming"]
// 3. filter - 过滤
Flux<Integer> evenNumbers = Flux.range(1, 10)
.filter(n -> n % 2 == 0); // [2, 4, 6, 8, 10]
// 4. take - 取前N个
Flux<Integer> first3 = Flux.range(1, 10)
.take(3); // [1, 2, 3]
// 5. skip - 跳过前N个
Flux<Integer> skip3 = Flux.range(1, 10)
.skip(3); // [4, 5, 6, 7, 8, 9, 10]
// 6. distinct - 去重
Flux<Integer> distinct = Flux.just(1, 2, 2, 3, 3, 3, 4)
.distinct(); // [1, 2, 3, 4]
5.3 组合操作符
java
// 1. zip - 拉链组合
Mono<String> name = Mono.just("Alice");
Mono<Integer> age = Mono.just(25);
Mono<String> combined = Mono.zip(name, age)
.map(tuple -> tuple.getT1() + " is " + tuple.getT2() + " years old");
// "Alice is 25 years old"
// 2. merge - 合并(不保证顺序)
Flux<String> flux1 = Flux.just("A", "B");
Flux<String> flux2 = Flux.just("C", "D");
Flux<String> merged = Flux.merge(flux1, flux2);
// 可能是 ["A", "C", "B", "D"] 或其他顺序
// 3. concat - 连接(保证顺序)
Flux<String> concatenated = Flux.concat(flux1, flux2);
// ["A", "B", "C", "D"]
// 4. zipWith - 两两组合
Flux<String> letters = Flux.just("A", "B", "C");
Flux<Integer> numbers = Flux.just(1, 2, 3);
Flux<String> zipped = letters.zipWith(numbers)
.map(tuple -> tuple.getT1() + tuple.getT2());
// ["A1", "B2", "C3"]
5.4 错误处理
java
// 1. onErrorReturn - 返回默认值
Mono<String> withDefault = Mono.error(new RuntimeException("Error"))
.onErrorReturn("Default Value");
// 2. onErrorResume - 降级到另一个流
Mono<User> withFallback = userService.findById(id)
.onErrorResume(error -> {
log.error("Error fetching user", error);
return Mono.just(User.getDefaultUser());
});
// 3. retry - 重试
Mono<String> withRetry = apiCall()
.retry(3) // 失败后重试3次
.timeout(Duration.ofSeconds(5)); // 超时5秒
// 4. doOnError - 错误时执行
Flux<String> flux = Flux.just("A", "B", "C")
.map(s -> {
if ("B".equals(s)) throw new RuntimeException("Error on B");
return s;
})
.doOnError(error -> log.error("Error occurred", error));
六、实战案例:用户信息聚合 💼
需求
查询用户信息时,需要聚合多个服务的数据:
- 用户基本信息(数据库)
- 用户头像(OSS服务)
- 用户积分(Redis)
- 用户订单数(数据库)
传统阻塞实现(MVC)
java
@Service
public class UserService {
public UserDTO getUserInfo(Long userId) {
// 1. 查询用户(100ms)
User user = userRepository.findById(userId).orElseThrow();
// 2. 查询头像(200ms)
String avatar = ossService.getAvatar(userId);
// 3. 查询积分(50ms)
Integer points = redisService.getPoints(userId);
// 4. 查询订单数(150ms)
Long orderCount = orderRepository.countByUserId(userId);
// 总耗时:100 + 200 + 50 + 150 = 500ms
return new UserDTO(user, avatar, points, orderCount);
}
}
问题:串行执行,总耗时500ms!😱
响应式实现(WebFlux)
java
@Service
public class UserReactiveService {
public Mono<UserDTO> getUserInfo(Long userId) {
// 1. 查询用户基本信息
Mono<User> userMono = userRepository.findById(userId);
// 2. 查询头像(并行)
Mono<String> avatarMono = ossService.getAvatar(userId);
// 3. 查询积分(并行)
Mono<Integer> pointsMono = redisService.getPoints(userId);
// 4. 查询订单数(并行)
Mono<Long> orderCountMono = orderRepository.countByUserId(userId);
// 5. 并行执行,组合结果
return Mono.zip(userMono, avatarMono, pointsMono, orderCountMono)
.map(tuple -> new UserDTO(
tuple.getT1(), // user
tuple.getT2(), // avatar
tuple.getT3(), // points
tuple.getT4() // orderCount
));
// 总耗时:max(100, 200, 50, 150) = 200ms
// 性能提升:500ms -> 200ms,快了2.5倍!🚀
}
}
七、背压(Backpressure)机制 🌊
7.1 什么是背压?
背压是指当数据生产速度 > 消费速度时,消费者可以向生产者发送信号,要求减慢速度。
7.2 生活类比
没有背压的场景:
工厂(生产者):疯狂生产商品,一秒1000件 📦📦📦
仓库(消费者):只能存放100件,容量已满!💥
结果:商品堆积如山,仓库爆炸!
有背压的场景:
erlang
工厂(生产者):准备生产...
仓库(消费者):"等等!我现在只能接收50件!"
工厂:好的,我慢一点,一秒50件。
仓库:完美!✅
7.3 代码示例
java
Flux.range(1, 1000) // 生产1000个数字
.onBackpressureBuffer(100) // 缓冲区大小100
.subscribe(new BaseSubscriber<Integer>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
request(10); // 我一次只要10个
}
@Override
protected void hookOnNext(Integer value) {
System.out.println("Processing: " + value);
// 处理完了再要下一批
request(10);
}
});
7.4 背压策略
java
// 1. Buffer - 缓冲(可能OOM)
Flux.range(1, 1000)
.onBackpressureBuffer(100); // 最多缓冲100个
// 2. Drop - 丢弃新数据
Flux.range(1, 1000)
.onBackpressureDrop(); // 处理不过来就丢掉新数据
// 3. Latest - 只保留最新数据
Flux.range(1, 1000)
.onBackpressureLatest(); // 只保留最新的数据
// 4. Error - 抛异常
Flux.range(1, 1000)
.onBackpressureError(); // 处理不过来就报错
八、WebFlux适用场景 🎯
8.1 ✅ 适合使用WebFlux的场景
-
IO密集型应用
- 大量数据库查询
- 频繁调用外部API
- 文件上传下载
- 实时数据流处理
-
高并发场景
- 秒杀系统
- 实时推送服务
- 聊天应用
- 股票行情推送
-
微服务网关
- Spring Cloud Gateway
- 路由转发
- 协议转换
-
事件流处理
- 日志收集
- 监控数据采集
- 实时数据分析
8.2 ❌ 不适合使用WebFlux的场景
-
CPU密集型计算
- 图像处理
- 视频编码
- 复杂算法计算
- 大数据离线计算
-
团队技能不足
- 学习曲线陡峭
- 调试困难
- 错误处理复杂
-
依赖阻塞式库
- JDBC(必须用R2DBC替代)
- 传统的HTTP客户端
- 阻塞式文件IO
-
简单CRUD应用
- 管理后台
- 简单的增删改查
- 低并发场景
8.3 决策树
markdown
你的应用是否有高并发需求(> 10K QPS)?
├─ 否 → 使用Spring MVC
└─ 是 → 应用是否IO密集型?
├─ 否(CPU密集型)→ 使用Spring MVC
└─ 是 → 团队是否熟悉响应式编程?
├─ 否 → 评估学习成本,可能先用MVC
└─ 是 → 依赖的库是否支持响应式?
├─ 否 → 评估迁移成本
└─ 是 → ✅ 使用WebFlux!
九、完整实战:商品搜索API 🛒
9.1 依赖配置
xml
<dependencies>
<!-- WebFlux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- R2DBC(响应式数据库驱动) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<!-- MySQL R2DBC驱动 -->
<dependency>
<groupId>dev.miku</groupId>
<artifactId>r2dbc-mysql</artifactId>
</dependency>
</dependencies>
9.2 实体类
java
@Data
@Table("products")
public class Product {
@Id
private Long id;
private String name;
private Double price;
private String category;
private Integer stock;
}
9.3 Repository
java
public interface ProductRepository extends ReactiveCrudRepository<Product, Long> {
Flux<Product> findByCategory(String category);
Flux<Product> findByPriceBetween(Double minPrice, Double maxPrice);
@Query("SELECT * FROM products WHERE name LIKE CONCAT('%', :keyword, '%')")
Flux<Product> searchByKeyword(String keyword);
}
9.4 Service
java
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private WebClient webClient; // 响应式HTTP客户端
// 搜索商品
public Flux<Product> searchProducts(String keyword) {
return productRepository.searchByKeyword(keyword)
.filter(p -> p.getStock() > 0) // 过滤库存为0的
.sort((p1, p2) -> p2.getPrice().compareTo(p1.getPrice())); // 按价格降序
}
// 获取商品详情(含外部评分)
public Mono<ProductDetailDTO> getProductDetail(Long productId) {
// 1. 查询商品信息
Mono<Product> productMono = productRepository.findById(productId);
// 2. 调用外部评分API(并行)
Mono<Double> ratingMono = webClient.get()
.uri("https://api.rating.com/product/{id}", productId)
.retrieve()
.bodyToMono(Double.class)
.onErrorReturn(0.0); // 失败时返回0.0
// 3. 组合结果
return Mono.zip(productMono, ratingMono)
.map(tuple -> {
Product product = tuple.getT1();
Double rating = tuple.getT2();
return new ProductDetailDTO(product, rating);
});
}
// 批量创建商品
public Flux<Product> batchCreate(List<Product> products) {
return Flux.fromIterable(products)
.flatMap(productRepository::save)
.doOnNext(p -> log.info("Created product: {}", p.getName()))
.doOnComplete(() -> log.info("All products created"));
}
}
9.5 Controller
java
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
// 搜索商品
@GetMapping("/search")
public Flux<Product> search(@RequestParam String keyword) {
return productService.searchProducts(keyword);
}
// 获取商品详情
@GetMapping("/{id}")
public Mono<ProductDetailDTO> getDetail(@PathVariable Long id) {
return productService.getProductDetail(id);
}
// 创建商品
@PostMapping
public Mono<Product> create(@RequestBody Product product) {
return productRepository.save(product);
}
// SSE实时推送(服务器推送事件)
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Product> streamProducts() {
return Flux.interval(Duration.ofSeconds(1))
.flatMap(tick -> productRepository.findAll())
.take(10); // 推送10次
}
}
十、常见问题与坑 ⚠️
坑1:阻塞操作
java
// ❌ 错误:在响应式流中使用阻塞操作
Mono<User> getUserMono = Mono.fromCallable(() -> {
Thread.sleep(1000); // 阻塞!会卡住整个Reactor线程池!
return userRepository.findById(id); // JDBC也是阻塞的!
});
// ✅ 正确:使用响应式数据库驱动
Mono<User> getUserMono = userR2dbcRepository.findById(id);
坑2:忘记订阅
java
// ❌ 错误:没有订阅,什么都不会执行!
Mono<String> mono = Mono.just("Hello");
mono.map(String::toUpperCase); // 不会执行!
// ✅ 正确:必须订阅
mono.map(String::toUpperCase)
.subscribe(System.out::println); // 触发执行
坑3:异常吞掉了
java
// ❌ 错误:异常被吞掉,不知道发生了什么
Mono<String> mono = Mono.error(new RuntimeException("Error"))
.subscribe();
// ✅ 正确:处理错误
mono.subscribe(
value -> System.out.println(value),
error -> System.err.println("Error: " + error.getMessage()),
() -> System.out.println("Completed")
);
坑4:阻塞订阅
java
// ❌ 错误:在主线程中阻塞等待结果(失去响应式意义)
String result = mono.block(); // 阻塞!
// ✅ 正确:返回Mono,让框架处理
@GetMapping("/user")
public Mono<User> getUser() {
return userService.findById(id); // 不要block!
}
十一、性能对比测试 📊
测试场景
模拟1000个并发请求,每个请求调用3个外部API(每个耗时100ms)。
| 技术栈 | 总耗时 | 平均响应时间 | 内存占用 | CPU使用率 |
|---|---|---|---|---|
| Spring MVC | 30秒 | 300ms | 2GB | 80% |
| Spring WebFlux | 10秒 | 100ms | 500MB | 40% |
结论:WebFlux在IO密集型场景下性能提升明显!
十二、学习路线图 🗺️
python
第1阶段:基础概念
├─ 响应式编程思想
├─ Mono & Flux基本使用
└─ 操作符(map、filter、flatMap)
第2阶段:进阶使用
├─ 错误处理
├─ 背压机制
└─ 调度器(Schedulers)
第3阶段:实战应用
├─ WebFlux Controller
├─ R2DBC数据库操作
└─ WebClient HTTP调用
第4阶段:高级特性
├─ Context传递
├─ Hot vs Cold Publisher
└─ 自定义操作符
第5阶段:生产实践
├─ 性能调优
├─ 监控与调试
└─ 最佳实践
十三、总结 🎉
核心要点
- WebFlux = 异步非阻塞 + 数据流
- Mono = 0或1个元素,Flux = 0到N个元素
- 适合IO密集型、高并发场景
- 不适合CPU密集型、团队不熟悉
- 背压机制防止生产者压垮消费者
口诀
python
响应式编程要记牢,
异步非阻塞是法宝。
Mono单个Flux多个,
数据流动像水泡。
操作符多达百种,
map、filter、flatMap用。
背压机制防爆炸,
生产消费要协调。
IO密集场景好,
CPU密集别乱跑。
团队技能要考虑,
生产环境稳定重要!
何时选择WebFlux?
✅ 使用WebFlux:
- 高并发(> 10K QPS)
- IO密集型(大量网络调用、数据库查询)
- 实时数据流
- 团队熟悉响应式编程
❌ 使用Spring MVC:
- 低并发(< 1K QPS)
- CPU密集型
- 简单CRUD
- 团队不熟悉响应式
参考资料 📚
下期预告: 134-Spring的循环依赖在构造器注入和多例模式下为何无法解决?🔄
编写时间:2025年
作者:技术文档小助手 ✍️
版本:v1.0
愿你的代码像水流一样优雅流畅! 🌊💙