Project Reactor是Java响应式编程库,提供Mono和Flux核心类型,支持非阻塞、背压及异步数据流处理。它是Spring WebFlux的基础,适用于构建高并发、低延迟的微服务与事件驱动应用,遵循Reactive Streams规范。
1. 响应式编程入门:从阻塞困境到数据流之美
2. 深入 Project Reactor:从原理到工程实践的全面指南
3. Flux 与 Mono:Project Reactor 核心响应式类型深度解析
4. Mono:Project Reactor 中最精巧的响应式原语
5. 创建 Flux/Mono 并订阅:Project Reactor 响应式编程的第一步
6. 程序化创建响应式序列:Flux.generate、Flux.create 与 Flux.push 深度解析
7. 线程调度与 Schedulers:Project Reactor 并发模型的核心引擎
8. 响应式流中的错误处理:Project Reactor 异常治理全体系
9. Sinks API:Project Reactor 中程序化发射数据的现代方案
1. 为什么需要响应式编程?
1.1 传统模型的瓶颈
在经典的 **Servlet 线程模型(Thread-per-Request)**中,每一个 HTTP 请求都会占用一个线程。当请求涉及数据库查询、远程 RPC 调用或文件 I/O 时,线程会阻塞等待,直到 I/O 完成。
线程-1: [处理请求] ──[████ 等待DB ██████]── [返回响应]
线程-2: [处理请求] ──[████████ 等待HTTP ████████]── [返回响应]
线程-3: [处理请求] ──[██ 等待Redis ██]── [返回响应]
在 Tomcat 默认 200 线程的配置下,一旦并发量超过线程池容量,新请求只能排队甚至被拒绝。线程成了最昂贵的资源瓶颈。
1.2 响应式编程的核心思想
响应式编程(Reactive Programming)将关注点从"线程等待数据"转变为"数据到达时通知处理":
- 异步非阻塞:I/O 操作不阻塞线程,通过回调/事件通知结果。
- 事件驱动:数据以"流"(Stream)的形式流动,开发者声明式地描述如何转换流。
- 背压控制:下游消费者可以主动控制上游的生产速率,避免被数据洪流淹没。
1.3 响应式宣言(Reactive Manifesto)四大原则
| 原则 | 含义 |
|---|---|
| 响应性(Responsive) | 系统及时响应,提供一致的服务质量 |
| 弹性(Resilient) | 面对故障仍能保持响应,通过复制、隔离实现 |
| 伸缩性(Elastic) | 在不同负载下保持响应性,动态伸缩资源 |
| 消息驱动(Message Driven) | 组件间通过异步消息通信,实现松耦合 |
2. Project Reactor 全景概览
Project Reactor 是由 Pivotal(现 VMware Tanzu / Broadcom)团队主导开发的 Java 响应式编程库,是 Spring WebFlux 的底层核心依赖,也是 Java 生态中最成熟、最广泛使用的 Reactive Streams 实现。
┌─────────────────────────────────────────────────────┐
│ Spring WebFlux │
│ (响应式 Web 框架) │
├─────────────────────────────────────────────────────┤
│ Project Reactor │
│ (响应式编程核心库) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Mono │ │ Flux │ │ Scheduler │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────┤
│ Reactive Streams (规范) │
│ Publisher / Subscriber / Subscription / Processor │
├─────────────────────────────────────────────────────┤
│ Java 9+ Flow API │
└─────────────────────────────────────────────────────┘
核心设计哲学:
- 函数式 + 声明式:用操作符链描述数据转换逻辑,而非命令式循环。
- 惰性求值(Lazy):操作符链在 subscribe() 之前不会执行任何逻辑。
- 可组合(Composable):Mono/Flux 可以像乐高积木一样自由组合。
- 零拷贝、低分配:内部大量使用对象池和无锁数据结构,追求极致性能。
Maven 依赖:
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.7.3</version> <!-- 请使用最新版本 -->
</dependency>
<!-- 测试支持 -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<version>3.7.3</version>
<scope>test</scope>
</dependency>
3. Reactive Streams 规范:四大基石
Project Reactor 严格实现了 Reactive Streams 规范(已纳入 Java 9 的 java.util.concurrent.Flow)。规范定义了四个核心接口:
// 1. 发布者 —— 数据源
public interface Publisher<T> {
void subscribe(Subscriber<? super T> subscriber);
}
// 2. 订阅者 —— 数据消费者
public interface Subscriber<T> {
void onSubscribe(Subscription s); // 订阅建立时回调
void onNext(T t); // 收到一个数据元素
void onError(Throwable t); // 发生不可恢复的错误
void onComplete(); // 数据流正常结束
}
// 3. 订阅关系 —— 背压的通道
public interface Subscription {
void request(long n); // 下游向上游请求 n 个元素
void cancel(); // 取消订阅
}
// 4. 处理器 —— 既是订阅者又是发布者
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {}
关键规则:
- onNext 可以被调用 0~N 次。
- onError 和 onComplete 是终端信号,二者互斥,最多出现一次。
- request(n) 中 n 必须 > 0,Long.MAX_VALUE 表示无界请求(即取消背压)。
4. 核心类型:Mono 与 Flux
Reactor 的一切围绕两个 Publisher 实现展开:
4.1 Mono:0 或 1 个元素
// 创建一个 Mono
Mono<String> mono = Mono.just("Hello Reactor");
// 从 CompletableFuture 桥接
Mono<User> userMono = Mono.fromFuture(userService.findByIdAsync(1L));
// 空 Mono
Mono<Void> empty = Mono.empty();
// 延迟创建(每次订阅都重新计算)
Mono<Long> delayed = Mono.delay(Duration.ofSeconds(3));
// 实际使用:查询单个用户
Mono<User> findUser(Long id) {
return userRepository.findById(id) // 返回 Mono<User>
.switchIfEmpty(Mono.error(new UserNotFoundException(id)))
.map(user -> {
user.setLastAccessTime(LocalDateTime.now());
return user;
})
.cache(Duration.ofMinutes(5)); // 缓存 5 分钟
}
4.2 Flux:0 到 N 个元素
// 从集合创建
Flux<Integer> numbers = Flux.fromIterable(List.of(1, 2, 3, 4, 5));
// 区间生成
Flux<Long> range = Flux.range(1, 100).map(Integer::longValue);
// 定时发射(每 500ms 一个元素)
Flux<Long> interval = Flux.interval(Duration.ofMillis(500));
// 编程式创建(适合桥接回调 API)
Flux<String> bridge = Flux.create(sink -> {
messageListener.onMessage(msg -> sink.next(msg.getBody()));
messageListener.onError(sink::error);
messageListener.onClose(sink::complete);
sink.onDispose(() -> messageListener.close());
});
4.3 Mono ↔ Flux 互转
Flux<Integer> flux = Flux.just(1, 2, 3);
Mono<Integer> first = flux.next(); // 取第一个 → Mono
Flux<Integer> back = first.flux(); // Mono → Flux
// collectList: Flux<T> → Mono<List<T>>
Mono<List<Integer>> listMono = flux.collectList();
// flatMapMany: Mono → Flux
Flux<Order> orders = userMono.flatMapMany(user -> orderRepo.findByUserId(user.getId()));
4.4 惰性求值:subscribe 才触发
// ⚠️ 这行代码什么都不会发生!
Flux<String> lazy = Flux.just("A", "B", "C")
.map(s -> {
System.out.println("Processing: " + s);
return s.toLowerCase();
});
// 必须 subscribe 才会真正执行
lazy.subscribe(System.out::println);
// 输出:
// Processing: A
// a
// Processing: B
// b
// Processing: C
// c
核心要义:Reactor 的操作符链是声明式蓝图,只有在 subscribe()(或 WebFlux 框架自动订阅)时才会真正执行。
5. 操作符体系:数据流的"瑞士军刀"
Reactor 提供了 250+ 个操作符,以下按功能分类梳理最常用的一组。
5.1 转换(Transform)
Flux.just("apple", "banana", "cherry")
.map(String::toUpperCase) // 1:1 同步转换
.flatMap(s -> queryDb(s)) // 1:N 异步转换(不保序)
.concatMap(s -> queryDb(s)) // 1:N 异步转换(保序)
.flatMapSequential(s -> queryDb(s), 4) // 并发度=4,保序
.subscribe(System.out::println);
5.2 过滤(Filter)
Flux.range(1, 100)
.filter(n -> n % 2 == 0) // 偶数
.distinct() // 去重
.skip(5) // 跳过前 5 个
.take(10) // 只取 10 个
.subscribe(System.out::println);
5.3 组合(Combine)
// merge: 交错合并(谁先到谁先出)
Flux<String> merged = Flux.merge(fluxA, fluxB);
// concat: 顺序拼接(A 完成后才开始 B)
Flux<String> concatenated = Flux.concat(fluxA, fluxB);
// zip: 一一配对
Flux<Tuple2<User, Order>> zipped = Flux.zip(userFlux, orderFlux);
// combineLatest: 取各流最新值组合
Flux<String> combined = Flux.combineLatest(
priceFlux, quantityFlux,
(price, qty) -> "Total: " + price * qty
);
5.4 聚合(Aggregate)
Flux.range(1, 10)
.reduce(0, Integer::sum) // Mono<Integer> = 55
.subscribe(System.out::println);
Flux.range(1, 10)
.scan(0, Integer::sum) // Flux<Integer>: 1,3,6,10,...,55
.subscribe(System.out::println);
Flux.just("a", "b", "a", "c", "b")
.collect(Collectors.groupingBy(s -> s, Collectors.counting()))
.subscribe(System.out::println); // {a=2, b=2, c=1}
5.5 窗口与缓冲(Window / Buffer)
// buffer: 每 3 个元素打包成 List
Flux.range(1, 10)
.buffer(3)
.subscribe(list -> System.out.println("Batch: " + list));
// Batch: [1, 2, 3]
// Batch: [4, 5, 6]
// Batch: [7, 8, 9]
// Batch: [10]
// window: 每 3 个元素切分为一个子 Flux
Flux.range(1, 10)
.window(3)
.flatMap(window -> window.reduce(0, Integer::sum))
.subscribe(System.out::println);
// 6, 15, 24, 10
6. 调度器(Scheduler):线程模型的精髓
Reactor 本身不绑定线程模型,通过 Scheduler 抽象来切换执行上下文。
6.1 内置调度器
| 调度器 | 工厂方法 | 适用场景 |
|---|---|---|
| immediate | Schedulers.immediate() | 当前线程直接执行(默认) |
| single | Schedulers.single() | 单线程,适合低延迟定时任务 |
| boundedElastic | Schedulers.boundedElastic() | 有界弹性线程池,包装阻塞调用 |
| parallel | Schedulers.parallel() | 固定大小(CPU 核心数),CPU 密集计算 |
6.2 subscribeOn vs publishOn
Flux.range(1, 5)
.map(i -> {
System.out.println("[map] " + Thread.currentThread().getName());
return i * 2;
})
.subscribeOn(Schedulers.boundedElastic()) // 影响上游:源头在哪个线程
.publishOn(Schedulers.parallel()) // 影响下游:切换后续操作的线程
.map(i -> {
System.out.println("[after publishOn] " + Thread.currentThread().getName());
return i + 1;
})
.subscribe();
核心区别:
- subscribeOn:决定订阅(数据源) 在哪个线程执行,整条链中只有第一个生效。
- publishOn:从该操作符开始,下游切换到指定调度器,可以出现多次。
6.3 包装阻塞调用的正确姿势
// ❌ 错误:在响应式链中直接调用阻塞方法
Mono<User> bad = Mono.just(blockingService.getUser());
// ✅ 正确:用 boundedElastic 包装
Mono<User> good = Mono.fromCallable(() -> blockingService.getUser())
.subscribeOn(Schedulers.boundedElastic());
7. 背压(Backpressure):响应式的灵魂
背压是 Reactive Streams 区别于普通异步回调的核心特性——下游可以告诉上游"我只要 N 个"。
7.1 背压策略
Flux.range(1, 1_000_000)
.onBackpressureBuffer(1024) // 缓冲:最多缓存 1024 个
// .onBackpressureDrop() // 丢弃:来不及处理的直接丢弃
// .onBackpressureLatest() // 只保留最新
// .onBackpressureError() // 溢出直接报错
.subscribe(new BaseSubscriber<>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
request(10); // 初始只请求 10 个
}
@Override
protected void hookOnNext(Integer value) {
// 慢速消费
process(value);
request(1); // 每处理完一个,再要一个
}
});
7.2 实际场景:数据库批量写入
Flux<Order> orderStream = orderSource.listen();
orderStream
.bufferTimeout(500, Duration.ofSeconds(2)) // 每 500 条或每 2 秒
.onBackpressureBuffer(64) // 最多缓存 64 个批次
.concatMap(batch -> // 顺序写入,避免并发冲突
dbClient.batchInsert(batch)
.then(Mono.delay(Duration.ofMillis(100))) // 限流
)
.subscribe();
8. 错误处理:优雅地应对失败
8.1 错误处理操作符
webClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.bodyToMono(User.class)
// 1. 捕获特定异常,返回兜底值
.onErrorResume(UserNotFoundException.class, e -> Mono.just(User.DEFAULT))
// 2. 重试(指数退避)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.maxBackoff(Duration.ofSeconds(10))
.filter(e -> e instanceof TransientException))
// 3. 超时控制
.timeout(Duration.ofSeconds(5))
// 4. 全局兜底
.onErrorReturn(User.FALLBACK)
.subscribe();
8.2 错误传播规则
onNext → onNext → onError → (终止,后续元素不再发出)
onNext → onNext → onComplete → (正常结束)
- 错误信号沿操作符链向下传播,直到被某个 onError* 操作符捕获。
- 未被捕获的错误会传递给最终的 Subscriber.onError()。
- 错误是终端事件:一旦发出,流即结束。
8.3 在 flatMap 中隔离错误
Flux.just(1, 2, 3, 4, 5)
.flatMap(i -> riskyCall(i)
.onErrorResume(e -> {
log.warn("Item {} failed: {}", i, e.getMessage());
return Mono.empty(); // 跳过失败项,不影响其他
})
)
.subscribe();
9. 与 Spring WebFlux 的深度集成
9.1 响应式 Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository userRepository;
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getUser(@PathVariable Long id) {
return userRepository.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@GetMapping
public Flux<User> listUsers(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return userRepository.findAll()
.skip((long) page * size)
.take(size);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<User> createUser(@Valid @RequestBody Mono<User> userMono) {
return userMono.flatMap(userRepository::save);
}
// SSE(Server-Sent Events)流式推送
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<User>> userStream() {
return userRepository.listenToChanges()
.map(user -> ServerSentEvent.<User>builder()
.id(user.getId().toString())
.event("user-update")
.data(user)
.build());
}
}
9.2 WebClient:响应式 HTTP 客户端
@Service
public class OrderService {
private final WebClient webClient;
public Mono<OrderDetail> getOrderDetail(Long orderId) {
return webClient.get()
.uri("/api/orders/{id}", orderId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, resp ->
Mono.error(new BusinessException("Order not found: " + orderId)))
.bodyToMono(Order.class)
.zipWith(
webClient.get()
.uri("/api/users/{id}", 0) // 占位
.retrieve()
.bodyToMono(User.class)
)
.map(tuple -> new OrderDetail(tuple.getT1(), tuple.getT2()))
.timeout(Duration.ofSeconds(3));
}
}
9.3 响应式数据库访问(R2DBC)
@Repository
public class ReactiveUserRepository {
private final R2dbcEntityTemplate template;
public Flux<User> findActiveUsers(int limit) {
Query query = Query.query(Criteria.where("status").is("ACTIVE"))
.sort(Sort.by(Sort.Direction.DESC, "createdAt"))
.limit(limit);
return template.select(query, User.class);
}
}
10. 工程最佳实践与常见陷阱
10.1 ✅ 最佳实践
① 保持链条完整,不要提前 subscribe
// ❌ 错误:在 Service 层 subscribe 了
public void process() {
flux.subscribe(); // 断开了与上层的响应式链路
}
// ✅ 正确:返回 Mono/Flux,让框架层 subscribe
public Mono<Result> process() {
return flux.collectList().map(Result::new);
}
② 使用 defer 延迟副作用
// ❌ 每次构建 Bean 时就执行了
Mono<Long> bad = Mono.just(System.currentTimeMillis());
// ✅ 每次 subscribe 时才执行
Mono<Long> good = Mono.defer(() -> Mono.just(System.currentTimeMillis()));
③ 善用 Context 传递请求级数据(替代 ThreadLocal)
Mono<User> getUser() {
return Mono.deferContextual(ctx -> {
String traceId = ctx.get("traceId");
return userRepository.findByTrace(traceId);
});
}
// 写入 Context
getUser()
.contextWrite(Context.of("traceId", UUID.randomUUID().toString()))
.subscribe();
④ 使用 StepVerifier 编写单元测试
@Test
void testFluxTransform() {
StepVerifier.create(
Flux.just(1, 2, 3).map(i -> i * 10)
)
.expectNext(10)
.expectNext(20)
.expectNext(30)
.verifyComplete();
}
@Test
void testErrorHandling() {
StepVerifier.create(
Mono.error(new RuntimeException("boom"))
.onErrorReturn(-1)
)
.expectNext(-1)
.verifyComplete();
}
10.2 ❌ 常见陷阱
| 陷阱 | 说明 | 修复 |
|---|---|---|
| 在响应式链中调用阻塞代码 | 阻塞 EventLoop 线程,导致吞吐骤降 | 用 subscribeOn(boundedElastic()) 包装 |
| 忘记 subscribe | 链不执行,数据"消失" | 确保最终有 subscribe 或返回给框架 |
| 在 map 中抛出 checked 异常 | 编译不过或异常被吞 | 用 Mono.fromCallable() + onErrorResume |
| 过度使用 block() | 退化为同步模型,丧失响应式优势 | 保持全链路异步 |
| 在 flatMap 中共享可变状态 | 并发竞争导致数据错乱 | 使用不可变对象或 concatMap |
| 忽略 Disposable | 无法取消订阅,可能内存泄漏 | 保存 Disposable,在适当时机 dispose() |
11. 性能调优与监控
11.1 关键调优参数
// 调整 boundedElastic 线程池
Schedulers.newBoundedElastic(
200, // 最大线程数
100_000, // 队列容量
"blocking-io",// 线程名前缀
60 // 空闲线程存活秒数
);
// 调整 parallel 调度器(默认 = CPU 核心数)
Schedulers.newParallel("compute", Runtime.getRuntime().availableProcessors());
11.2 使用 Hooks 全局监控
// 应用启动时注册
Hooks.onOperatorDebug(); // ⚠️ 仅开发环境!生产环境性能开销极大
// 生产环境推荐:Reactor Debug Agent(Java Agent 方式,零运行时开销)
// -javaagent:reactor-tools-agent.jar
11.3 Micrometer 指标集成
// Spring Boot Actuator + Micrometer 自动采集 Reactor 指标
// application.yml
management:
metrics:
tags:
application: my-service
endpoints:
web:
exposure:
include: metrics, prometheus
关注的关键指标:
- reactor.subscriber.count:活跃订阅数
- reactor.requested:背压请求量
- scheduler.threads.active:调度器活跃线程数
- scheduler.queue.size:任务队列积压
11.4 避免不必要的对象分配
// ❌ 每次 map 都创建新 String
flux.map(i -> "item-" + i);
// ✅ 如果下游只需要数值,避免无谓转换
flux.map(i -> i * 2);
// ✅ 使用 Flux.using 管理资源生命周期
Flux.using(
() -> openConnection(), // 资源创建
conn -> Flux.fromIterable(conn.query()), // 使用资源
conn -> conn.close() // 资源释放(finally)
);
12. 总结与展望
何时选择 Reactor?
| 场景 | 推荐度 | 说明 |
|---|---|---|
| API 网关 / BFF 层 | ⭐⭐⭐⭐⭐ | 大量 I/O 转发,响应式收益最大 |
| 高并发 WebSocket / SSE | ⭐⭐⭐⭐⭐ | 长连接场景天然适配 |
| 流式数据处理(ETL) | ⭐⭐⭐⭐ | 背压机制防止 OOM |
| 微服务间 HTTP/RPC 调用 | ⭐⭐⭐⭐ | WebClient + 全链路非阻塞 |
| CPU 密集型计算 | ⭐⭐ | 收益有限,考虑 Fork/Join 或并行流 |
| 简单 CRUD 后台管理 | ⭐⭐ | 复杂度收益比不高,传统 MVC 足够 |
核心要点回顾
1. Mono = 0|1 个元素,Flux = 0..N 个元素
2. 一切皆惰性 —— subscribe() 才触发执行
3. subscribeOn 影响源头,publishOn 影响下游
4. 阻塞代码必须隔离到 boundedElastic
5. 背压是权利,也是责任 —— 合理设置 buffer/drop 策略
6. 错误是终端信号 —— 用 onErrorResume/retryWhen 优雅处理
7. 不要 block(),不要提前 subscribe,不要在 map 里做 I/O
8. 测试用 StepVerifier,调试用 reactor-tools agent
展望
随着 Virtual Threads(虚拟线程,JDK 21+) 的成熟,Java 生态正在迎来"同步写法 + 非阻塞性能"的新范式。Spring Framework 7 已提供对虚拟线程的一级支持。但这并不意味着 Reactor 会退出舞台——在流式处理、背压控制、事件驱动架构、SSE/WebSocket 长连接等场景中,响应式编程模型依然具有不可替代的优势。未来的最佳实践,很可能是 “虚拟线程处理请求级并发 + Reactor 处理流级并发” 的混合模型。

1万+

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



