作为 Java 后端开发者,你对 Flux 和 Mono 的理解,是掌握 Spring WebFlux 响应式编程的核心基石。它们不是简单的“返回类型”,而是响应式流的语义化容器,决定了你如何处理异步、非阻塞、背压驱动的数据流。
以下是一份最标准、最实用、带详细中文注释的实战说明文档,结合真实业务场景,帮你彻底搞懂 Flux 与 Mono 的区别、作用与使用规范。
📄 Flux 与 Mono:Spring WebFlux 响应式编程的核心类型详解
版本:2025年10月
适用对象:Java 后端开发者 / 响应式系统架构师
目标:彻底理解 Flux 与 Mono 的语义、使用场景、最佳实践与代码规范
一、什么是 Flux 和 Mono?
✅ 官方定义(Project Reactor)
-
Mono<T>:表示一个最多包含 0 或 1 个元素的异步序列。语义:“单个结果”或“无结果”,如:查询一条用户、保存一个订单、删除一个文件。
-
Flux<T>:表示一个可能包含 0 到 N 个元素的异步序列。语义:“多个结果”或“数据流”,如:查询所有用户、实时推送消息、分页加载数据。
💡 关键认知:
Mono=Optional<T>的异步版本(0 或 1)Flux=Stream<T>的异步、背压、非阻塞版本(0 到 ∞)
它们都是 Reactive Streams Publisher 的实现,遵循 Reactive Streams 规范(支持背压、非阻塞、事件驱动)。
二、Flux 与 Mono 的核心特点对比
| 特性 | Mono<T> | Flux<T> |
|---|---|---|
| 元素数量 | 0 或 1 个 | 0 到 N 个(可无限) |
| 语义含义 | 单值操作(查询、创建、删除) | 多值操作(列表、流、事件) |
| 是否支持背压 | ✅ 是(但无“多元素”背压意义) | ✅ 是(核心特性,控制生产/消费速率) |
| 常用操作符 | map, flatMap, switchIfEmpty, onErrorResume, block() | map, flatMap, filter, take, buffer, window, concatMap |
| 是否可遍历 | ❌ 不可直接遍历(无 forEach) | ❌ 不可直接遍历(无 forEach) |
| 终端操作 | block(), subscribe(), toFuture() | blockLast(), subscribe(), collectList() |
| 典型应用场景 | 查询用户、登录验证、保存记录、删除资源 | 查询用户列表、WebSocket 消息推送、分页数据、日志流 |
| 线程模型 | 在异步线程中完成,不阻塞调用线程 | 同上,但支持多个元素的异步发射 |
⚠️ 重要提醒:
永远不要在响应式链中使用.block()—— 它会阻塞线程,破坏非阻塞架构!仅用于测试或极少数边缘场景。
三、为什么必须使用 Flux 或 Mono?(不使用会怎样?)
❌ 错误做法:直接返回原始类型(如 List<User>)
@GetMapping("/users")
public List<User> getUsers() { // ❌ 错误!违反响应式原则
return userService.findAll(); // 内部是阻塞 JDBC 调用!
}
问题:
- 你写的是“响应式控制器”,但底层仍是阻塞调用。
- 线程被数据库查询卡死 → WebFlux 的非阻塞优势全无。
- 服务器在高并发下迅速耗尽线程 → 性能暴跌。
✅ 正确做法:返回 Flux<User> 或 Mono<User>
@GetMapping("/users")
public Flux<User> getUsers() { // ✅ 正确!语义清晰、非阻塞
return userService.findAll(); // 内部使用 R2DBC 或 WebClient,异步非阻塞
}
优势:
- 数据库查询不阻塞 Tomcat/Netty 线程。
- 数据“就绪”时自动推送,客户端可流式接收。
- 支持背压:客户端处理慢?服务端自动减缓发送速度。
- 兼容 WebSocket、SSE、HTTP/2 流式响应。
✅ 结论:
使用 Flux/Mono 不是“为了装逼”,而是为了实现真正的非阻塞、高并发、低资源消耗的响应式架构。
四、Flux 与 Mono 的详细使用示例(带中文注释)
✅ 示例 1:使用 Mono —— 查询单个用户(标准 CRUD)
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import java.time.Duration;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository; // 假设是响应式仓库(R2DBC / ReactiveMongo)
/**
* 根据 ID 查询单个用户 —— 使用 Mono
* 语义:要么找到一个用户,要么没找到(空)
* 返回类型:Mono<User> —— 表示“一个结果”或“无结果”
*/
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getUserById(@PathVariable Long id) {
// 1. 调用响应式仓库,返回 Mono<User>(异步非阻塞)
Mono<User> userMono = userRepository.findById(id);
// 2. 如果用户不存在,返回 404;否则返回 200
// 使用 switchIfEmpty:当 Mono 为空时,提供备选方案
return userMono
.map(user -> ResponseEntity.ok(user)) // ✅ 找到用户 → 返回 200 OK
.switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); // ❌ 未找到 → 返回 404 Not Found
}
/**
* 创建新用户 —— 使用 Mono
* 语义:创建一个用户,返回创建后的结果(含 ID)
*/
@PostMapping
public Mono<ResponseEntity<User>> createUser(@RequestBody User user) {
// 1. 保存用户(异步非阻塞)
Mono<User> savedUserMono = userRepository.save(user);
// 2. 保存成功后,返回 201 Created
return savedUserMono
.map(savedUser -> ResponseEntity.status(HttpStatus.CREATED).body(savedUser))
.onErrorResume(e -> {
// 3. 异常处理:如数据库冲突、校验失败
return Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null));
});
}
/**
* 删除用户 —— 使用 Mono<Void>
* 语义:执行一个“无返回值”的操作,但需确认是否成功
*/
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deleteUser(@PathVariable Long id) {
// 1. 删除操作返回 Mono<Void>,表示“操作完成”但无数据
Mono<Void> deleteResult = userRepository.deleteById(id);
// 2. 成功时返回 204 No Content(标准 REST 语义)
// 使用 then:表示“忽略前面的结果,只关心完成”
return deleteResult
.then(Mono.just(ResponseEntity.noContent().build())) // 成功 → 204
.onErrorResume(e -> Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build())); // 失败 → 500
}
}
✅ 关键点:
map():转换成功结果switchIfEmpty():处理“空值”场景onErrorResume():优雅降级,避免服务崩溃then():用于Mono<Void>,表示“操作完成,忽略结果”
✅ 示例 2:使用 Flux —— 查询用户列表 + 实时推送(流式场景)
import reactor.core.publisher.Flux;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository;
/**
* 获取所有用户列表 —— 使用 Flux<User>
* 语义:返回多个用户,可能数量很大(1000+),适合流式传输
* 客户端可通过 HTTP/2 流式接收,无需等待全部加载完成
*/
@GetMapping("/all")
public Flux<User> getAllUsers() {
// 1. 调用响应式仓库,返回 Flux<User>(异步发射多个元素)
return userRepository.findAll();
// 👉 内部可能是:SELECT * FROM users WHERE active = true(R2DBC 非阻塞查询)
}
/**
* 分页获取用户(每页 10 条)—— 使用 Flux + take + skip
* 语义:只取第 11~20 条数据,避免一次性加载全部
*/
@GetMapping("/page")
public Flux<User> getUsersPage(@RequestParam int page, @RequestParam int size) {
int skipCount = (page - 1) * size;
return userRepository.findAll()
.skip(skipCount) // 跳过前 N 条
.take(size); // 只取 size 条
}
/**
* 实时监控用户登录事件(WebSocket/SSE 场景)
* 语义:用户登录后,服务端持续推送事件,客户端保持连接
*/
@GetMapping(value = "/events", produces = "text/event-stream")
public Flux<UserLoginEvent> streamUserLoginEvents() {
// 1. 假设有一个响应式事件源:用户登录事件流(来自消息队列或缓存发布)
Flux<UserLoginEvent> loginEvents = userEventPublisher.loginEvents();
// 2. 过滤只推送“成功登录”的事件
return loginEvents
.filter(event -> event.getStatus() == UserLoginEvent.Status.SUCCESS)
.doOnNext(event -> System.out.println("🔥 登录事件: " + event.getUsername()))
.retryWhen(retrySpec -> retrySpec.delayElements(java.time.Duration.ofSeconds(1))) // 失败重试
.onErrorResume(e -> {
// 3. 出错时发送一个“系统异常”事件,保持流不中断
return Flux.just(new UserLoginEvent("SYSTEM", UserLoginEvent.Status.ERROR, e.getMessage()));
});
}
/**
* 批量导入用户(从 CSV 流中读取)—— 使用 Flux + buffer + flatMap
* 语义:逐行读取 CSV,每 10 行批量保存,提升数据库效率
*/
@PostMapping("/import")
public Mono<Long> importUsersFromStream(Flux<String> csvLines) {
return csvLines
.filter(line -> !line.trim().isEmpty()) // 过滤空行
.map(line -> User.fromCsvLine(line)) // 转换为 User 对象
.buffer(10) // 每 10 个用户打包成一个 List<User>
.flatMap(userList -> userRepository.saveAll(userList)) // 批量保存(异步)
.count(); // 返回总共保存了多少用户
}
}
✅ 关键点:
filter():过滤符合条件的元素skip()/take():分页、限流buffer(n):每 N 个元素打包成一个集合(适合批量操作)flatMap():将每个元素映射为一个Mono或Flux,并“扁平化”为一个流(非嵌套)doOnNext():用于日志、埋点,不影响流retryWhen():优雅重试,避免瞬时失败导致流中断count():终端操作,返回总数(Mono)
✅ 示例 3:Flux 与 Mono 的组合使用 —— 业务聚合场景(真实项目)
场景:用户登录后,需同时获取:
- 用户基本信息(
Mono<User>)- 用户权限列表(
Flux<Permission>)- 最近 5 条操作日志(
Flux<Log>)
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class DashboardController {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
@Autowired
private LogService logService;
/**
* 用户登录后,聚合其仪表盘数据
* 语义:一个用户(Mono) + 多个权限(Flux) + 多条日志(Flux)
* 输出:一个包含完整信息的 DTO
*/
@GetMapping("/dashboard/{userId}")
public Mono<DashboardResponse> getDashboard(@PathVariable Long userId) {
// 1. 获取用户信息(单个结果 → Mono)
Mono<User> userMono = userService.findById(userId);
// 2. 获取权限列表(多个结果 → Flux)
Flux<Permission> permissionsFlux = permissionService.findByUserId(userId);
// 3. 获取最近 5 条日志(多个结果 → Flux)
Flux<Log> logsFlux = logService.findRecent(userId, 5);
// 4. 使用 zipWith 组合:必须等所有流都完成,才合并结果
// 注意:zipWith 是“并行等待”,不是串行
return userMono
.zipWith(permissionsFlux.collectList(), (user, permissions) -> new UserWithPermissions(user, permissions))
.zipWith(logsFlux.collectList(), (userWithPerms, logs) -> {
// 5. 最终合并成完整响应对象
return new DashboardResponse(
userWithPerms.user(),
userWithPerms.permissions(),
logs
);
});
}
// 👇 辅助数据结构(仅用于示例)
record UserWithPermissions(User user, List<Permission> permissions) {}
record DashboardResponse(User user, List<Permission> permissions, List<Log> logs) {}
}
✅ 关键点:
zipWith():必须等所有流完成,才合并结果(类似Promise.all())collectList():将Flux<T>转为Mono<List<T>>,用于组合- 不推荐使用
.block():如userMono.block()→ 会破坏非阻塞特性- 整个链路无阻塞线程,Netty 线程始终在处理其他请求
五、Flux 与 Mono 的选择标准(生产环境最佳实践)
| 你想要什么? | 选择 | 理由 |
|---|---|---|
| 查询一条用户、一个订单、一个配置 | ✅ Mono<T> | 语义清晰:要么有,要么无 |
| 创建/更新/删除一个资源 | ✅ Mono<Void> 或 Mono<T> | 操作完成即返回,无多结果 |
| 查询用户列表、商品列表、分页数据 | ✅ Flux<T> | 数据可能很多,适合流式传输 |
| 推送实时消息(WebSocket、SSE) | ✅ Flux<T> | 数据是持续产生的“流” |
| 批量处理(如导入 1000 条记录) | ✅ Flux<T> + buffer() + flatMap() | 避免内存溢出,提高吞吐 |
| 聚合多个异步服务(用户+权限+日志) | ✅ Mono<Combined> | 用 zipWith + collectList() 组合多个流 |
| 需要“立即返回”或测试 | ⚠️ block()(仅限测试) | 生产环境禁止使用! |
| 需要“超时”或“重试” | ✅ timeout() / retryWhen() | 响应式流原生支持,优雅可控 |
六、绝对禁止的错误写法(生产环境踩坑清单)
| 错误写法 | 问题 | 正确做法 |
|---|---|---|
Flux<User> users = userRepository.findAll(); return users.block(); | 阻塞线程,破坏响应式架构 | 直接返回 Flux<User>,让客户端消费 |
Mono<User> user = userService.findById(id); if (user.block() != null) { ... } | 线程被阻塞,高并发下服务器崩溃 | 使用 map() / switchIfEmpty() 处理逻辑 |
return userRepository.findAll().toList().block(); | 一次性加载全部数据到内存,可能 OOM | 返回 Flux<User>,让客户端流式接收 |
Flux<User> flux = ...; flux.forEach(System.out::println); | forEach 是阻塞操作,不适用于响应式流 | 使用 doOnNext() 记录日志 |
✅ 黄金法则:
“你拿到 Flux/Mono,就把它当‘未来的结果’,不要去‘拿’它,而是‘告诉’它该怎么做。”
七、总结:一句话记忆口诀
“单值用 Mono,多值用 Flux;不阻塞、不 block,用 map/zip/flatMap 组合。”
| 场景 | 类型 | 类比 |
|---|---|---|
| 查一个用户 | Mono<User> | Optional<User> 的异步版 |
| 查一堆用户 | Flux<User> | Stream<User> 的异步、背压版 |
| 创建一个订单 | Mono<Order> | 一次操作,一个结果 |
| 推送心跳消息 | Flux<Heartbeat> | 持续不断的数据流 |
| 聚合三个服务 | Mono<Dashboard> | 三个异步流,等齐了再合并 |
✅ 附录:常用操作符速查表(生产环境必备)
| 操作符 | 作用 | 适用类型 |
|---|---|---|
map() | 转换元素 | Mono / Flux |
flatMap() | 将元素映射为另一个 Mono/Flux,并扁平化 | Mono / Flux |
filter() | 过滤元素 | Flux |
switchIfEmpty() | 为空时提供备选 | Mono |
collectList() | 将 Flux 转为 Mono | Flux |
zipWith() | 并行组合两个 Publisher | Mono / Flux |
buffer(n) | 每 n 个元素打包成一个 List | Flux |
take(n) | 只取前 n 个元素 | Flux |
skip(n) | 跳过前 n 个元素 | Flux |
timeout() | 超时自动失败 | Mono / Flux |
retryWhen() | 自定义重试策略 | Mono / Flux |
doOnNext() | 仅用于日志/埋点,不影响流 | Mono / Flux |
onErrorResume() | 出错时降级处理 | Mono / Flux |
📌 结语:响应式不是“新语法”,而是“新思维”
你不需要学会所有操作符,但必须理解 Flux 和 Mono 的语义本质:
Flux 是“数据流”,Mono 是“单次事件”。
你不是在“调用方法”,你是在“声明一个异步过程”。
当你用 Flux<User> users = service.findAll() 时,
你不是在“获取列表”,你是在声明:“请在准备好时,把每个用户发给我”。
这才是响应式编程的精髓。
✅ 建议:
从今天起,在你的 Spring Boot 3 项目中:
- 所有数据库查询 → 返回
Flux<T>或Mono<T>- 所有外部 HTTP 调用 → 使用
WebClient,返回Mono<...>或Flux<...>- 所有控制器方法 → 绝不返回
List<T>、T、ResponseEntity<T>(除非是测试)

7164

被折叠的 条评论
为什么被折叠?



