文章目录
- 一、前言
- [二、什么是 Reactor](#二、什么是 Reactor)
-
- [1. 核心概念](#1. 核心概念)
- [2. 与传统编程的比较](#2. 与传统编程的比较)
- 三、Reactor
-
- [1. Mono 和 Flux](#1. Mono 和 Flux)
-
- [1.1 简单示例](#1.1 简单示例)
- [1.2 Mono 和 Flux 的互转](#1.2 Mono 和 Flux 的互转)
- [1.3 关键特性](#1.3 关键特性)
- [2. 常用操作符](#2. 常用操作符)
-
- [2.1 Flux API](#2.1 Flux API)
-
- [2.1.1 创建类 API(最基础,用于生成 Flux)](#2.1.1 创建类 API(最基础,用于生成 Flux))
- [2.1.2 转换类 API(处理数据格式/结构)](#2.1.2 转换类 API(处理数据格式/结构))
- [2.1.3 过滤类 API(筛选元素)](#2.1.3 过滤类 API(筛选元素))
- [2.1.4 组合类 API(多 Flux 合并)](#2.1.4 组合类 API(多 Flux 合并))
- [2.1.5 错误处理 API(异常兜底)](#2.1.5 错误处理 API(异常兜底))
- [2.1.6 生命周期 API](#2.1.6 生命周期 API)
- [2.2 Mono 常用 API](#2.2 Mono 常用 API)
-
- [2.2.1 创建类 API](#2.2.1 创建类 API)
- [2.2.2 转换/兜底 API(Mono 特有)](#2.2.2 转换/兜底 API(Mono 特有))
- [2.2.3 组合类 API](#2.2.3 组合类 API)
- [2.2.4 阻塞获取结果(测试/特殊场景)](#2.2.4 阻塞获取结果(测试/特殊场景))
- [2.3 通用高频 API(Flux/Mono 都适用)](#2.3 通用高频 API(Flux/Mono 都适用))
- [2.4 高级 API](#2.4 高级 API)
-
- [2.4.1 combineLatest](#2.4.1 combineLatest)
- [2.4.2 contextWrite](#2.4.2 contextWrite)
- [2.4.3 materialize](#2.4.3 materialize)
- [2.4.4 metrics](#2.4.4 metrics)
- [3. 高级特性](#3. 高级特性)
-
- [3.1 背压](#3.1 背压)
-
- [3.1.1 核心特点](#3.1.1 核心特点)
- [3.1.2 实现原理](#3.1.2 实现原理)
- [3.2 调度器](#3.2 调度器)
-
- [3.2.1 内置的核心调度器](#3.2.1 内置的核心调度器)
- [2.3.2 使用示例](#2.3.2 使用示例)
- [2.3.3 进阶用法](#2.3.3 进阶用法)
- 四、扩展
-
- [1. Reactor 与 RxJava 的关系](#1. Reactor 与 RxJava 的关系)
- [2. Reactor 与 I/O 多路复用](#2. Reactor 与 I/O 多路复用)
-
- [2.1 概念说明](#2.1 概念说明)
- [2.2 核心区别](#2.2 核心区别)
- [2.3 实际关联](#2.3 实际关联)
- [2.4 总结](#2.4 总结)
- 五、参考内容
一、前言
本篇作为 Reactor 响应式编程的笔记文章,因为以前虽然有过学习,但是长时间不使用又忘了,恰逢某些机缘巧合又需要了该特性的使用,趁着回忆的时候将笔记记录下来。
如果后面有机会,就将"机缘巧合"的部分也写下来。
二、什么是 Reactor
Reactor 是基于 Reactive Streams(JVM 平台的响应式流标准)实现的一套响应式编程库,专为 Java 8+ 设计,是 Spring 生态(如 Spring WebFlux、Spring Data Reactive)的核心响应式组件。
这里要注意一个易混点:
- 操作系统层面有一个 Reactor 设计模式(也叫 "反应器模式"),它是多路复用的一种应用模式:用一个线程监听 I/O 事件(多路复用),事件就绪后分发给工作线程处理;
- 而我们说的 Reactor 响应式编程库,是基于 "Reactor 设计模式 + Reactive Streams 标准" 封装的应用层库。
简单说:
- 操作系统的 Reactor 模式 ≈ 多路复用 + 事件分发;
- Reactor 响应式编程库 = 基于 Reactor 模式的应用层异步编程框架。
1. 核心概念
响应式编程的本质是:以异步、非阻塞的方式处理数据流,并且数据流的生产者和消费者之间能实现"背压(Backpressure)"控制------简单说就是消费者可以告诉生产者"我处理不过来了,你慢一点",避免数据积压导致的内存溢出。
Reactor 的核心是两个核心类型:
Mono<T>:表示 0 或 1 个元素的异步序列(比如查询单个用户、执行一次数据库操作)。Flux<T>:表示 0 到 N 个元素的异步序列(比如查询用户列表、处理流式数据)。
以下面的简单示例来说明与传统同步编程的区别 :
-
传统同步编程(阻塞):
java// 同步查询用户,主线程会阻塞直到结果返回 public User getUserById(Long id) { // 假设这是数据库查询,会阻塞线程 return userRepository.findById(id); } -
Reactor 响应式编程(异步非阻塞):
java// 异步查询用户,主线程不会阻塞,结果通过回调处理 public Mono<User> getUserById(Long id) { // 返回Mono,代表"未来可能有一个User结果" return userRepository.findById(id); } // 使用方式, 需要注意:getUserById(1L) 返回一个 Mono 对象时,其查询并没有开始,只有调用了 Mono.subscribe 方法后才会真正执行。 getUserById(1L) .subscribe( user -> System.out.println("获取到用户:" + user.getName()), // 成功回调 error -> System.err.println("查询失败:" + error.getMessage()), // 异常回调 () -> System.out.println("操作完成") // 完成回调 );
2. 与传统编程的比较
传统编程(同步阻塞)的核心问题是:线程会被阻塞在 IO 操作(如数据库、网络请求)上,导致线程资源浪费,系统并发能力受限。Reactor 解决的正是这些问题,核心优势如下:
| 维度 | 传统编程(同步阻塞) | Reactor 响应式编程(异步非阻塞) |
|---|---|---|
| 线程模型 | 一个请求占用一个线程,直到结束 | 少量线程处理大量请求(事件驱动) |
| IO 处理 | 阻塞等待 IO 结果 | 非阻塞,IO 完成后通过回调处理 |
| 背压支持 | 无,生产者可能压垮消费者 | 内置背压,消费者可控数据流速度 |
| 错误处理 | 基于 try-catch,同步处理 | 流式错误处理(如 onErrorReturn) |
| 并发能力 | 受线程池大小限制,高并发易卡顿 | 高并发下资源占用少,响应更稳定 |
- 高并发的 Web 服务(如 Spring WebFlux 接口);
- 大量 IO 操作的场景(如批量查询数据库、调用第三方接口);
- 需要流式处理数据的场景(如实时日志分析、消息推送)。
三、Reactor
1. Mono 和 Flux
Reactor 提供了两个核心发布者类型:Mono 和 Flux,二者基本具备相同的 API 。区别在于 Mono 处理 0/1 个元素的异步场景(单结果),Flux 处理 0-N 个元素的异步场景(多结果);
1.1 简单示例
下面我们提供个简单示例来说明 Mono 和 Flux 的区别如下:
java
public static void main(String[] args) {
mono();
flux();
}
private static void flux() {
// Flux 的创建方式
Flux<Integer> flux1 = Flux.just(1, 2, 3, 4, 5);
Flux<Integer> flux2 = Flux.range(1, 10);
Flux<String> flux3 = Flux.fromIterable(Arrays.asList("A", "B", "C"));
// 订阅消费(会发送1,2,3,4,5 五个元素)
Flux.range(1, 5)
.subscribe(
value -> System.out.println("值: " + value),
error -> error.printStackTrace(),
() -> System.out.println("流结束")
);
}
private static void mono() {
// Mono 的创建方式
Mono<String> mono1 = Mono.just("Hello");
Mono<String> mono2 = Mono.empty();
Mono<String> mono3 = Mono.fromCallable(() -> "World");
// 订阅消费 (只会发送一个 "Hello Reactor" 元素)
Mono.just("Hello Reactor")
.subscribe(
value -> System.out.println("收到: " + value),
error -> System.err.println("错误: " + error),
() -> System.out.println("完成")
);
}
简单来说可以根据返回结果的数量选择 ------ 查单个对象用 Mono,查列表 / 流式数据用 Flux。
1.2 Mono 和 Flux 的互转
java
// 1. Flux 转 Mono:取第一个元素(若为空则返回空 Mono)
Flux<String> flux = Flux.just("A", "B", "C");
Mono<String> firstMono = flux.next(); // 取第一个元素 "A"
// 2. Flux 转 Mono:收集所有元素为集合
Mono<List<String>> listMono = flux.collectList(); // 结果:["A","B","C"]
// 3. Mono 转 Flux:把单个元素转为 Flux
Mono<String> mono = Mono.just("X");
Flux<String> monoToFlux = mono.flux(); // 结果:[X]
// 4. 空 Mono 转 Flux:返回空 Flux
Mono<Void> emptyMono = Mono.empty();
Flux<Void> emptyFlux = emptyMono.flux();
1.3 关键特性
Mono 和 Flux 具备如下特性:
- 惰性执行:Mono 和 Flux 定义后不会立即执行,只有调用 subscribe() 或 block()(阻塞获取结果)时才会触发;
- 背压支持:两者都内置背压机制,消费者可以控制生产者的数据流速度;
- 错误处理:不要用 try-catch 包裹 Mono/Flux,而是使用 onErrorReturn、onErrorResume 等操作符处理异常;
- 避免阻塞:除非特殊场景(如测试),否则不要在生产环境使用 block(),会失去异步非阻塞的优势。
简单理解
- Mono 就像 "异步版的 Optional"------ 要么返回一个结果,要么空,要么出错;
- Flux 就像 "异步版的 List"------ 可以返回多个结果,也可以为空,还能流式处理。
2. 常用操作符
Mono 和 Flux 有诸多 API,所有 API 都遵循惰性执行 原则:调用 API 只是定义数据流的处理逻辑,只有调用 subscribe()/block() 才会触发执行;且所有操作符都是无副作用的(不会修改原数据流,而是返回新的 Flux/Mono)。
下面我们根据场景来介绍 Mono 和 Flux 的常见 API。(Mono 大部分 API 与 Flux 通用,因此这里先以 Flux 为例)
大部分 API 与 Java Stream 的 API 类似,因此理解起来并不复杂。
2.1 Flux API
2.1.1 创建类 API(最基础,用于生成 Flux)
| API 名称 | 作用 | 示例代码 |
|---|---|---|
just() |
从固定元素创建 | Flux.just("A", "B", "C") → 生成包含 A、B、C 的 Flux |
fromIterable() |
从集合/迭代器创建 | Flux.fromIterable(Arrays.asList(1,2,3)) |
range() |
生成整数范围序列 | Flux.range(1, 5) → 生成 1、2、3、4、5 |
interval() |
按时间间隔生成无限流(需 take 限制) | Flux.interval(Duration.ofSeconds(1)).take(3) → 每1秒生成一个Long,共3个 |
empty() |
创建空 Flux | Flux.empty() → 无元素,直接触发 onComplete() |
error() |
创建出错的 Flux | Flux.error(new RuntimeException("失败")) |
2.1.2 转换类 API(处理数据格式/结构)
| API 名称 | 作用 | 示例代码 |
|---|---|---|
map() |
一对一转换元素(同 Stream.map) | Flux.range(1,3).map(num -> num * 2) → 2、4、6 |
flatMap() |
一对多转换(异步,顺序不保证,这里注意与 Java Stream 的逻辑不同) | Flux.just("A","B").flatMap(s -> Flux.just(s + "1", s + "2")) → A1、B1、A2、B2(顺序不定) |
concatMap() |
一对多转换(异步,保证顺序) | Flux.just("A","B").concatMap(s -> Flux.just(s + "1", s + "2")) → A1、A2、B1、B2 |
collectList() |
收集所有元素为 List(转 Mono) | Flux.range(1,3).collectList() → Mono[1,2,3] |
collectMap() |
收集所有元素为 Map(转 Mono) | Flux.just(new User(1,"张三")).collectMap(User::getId) → Mono{1: 张三} |
2.1.3 过滤类 API(筛选元素)
| API 名称 | 作用 | 示例代码 |
|---|---|---|
filter() |
按条件过滤元素 | Flux.range(1,5).filter(num -> num % 2 == 0) → 2、4 |
take() |
取前 N 个元素 | Flux.range(1,5).take(3) → 1、2、3 |
takeLast() |
取后 N 个元素 | Flux.range(1,5).takeLast(2) → 4、5 |
skip() |
跳过前 N 个元素 | Flux.range(1,5).skip(2) → 3、4、5 |
distinct() |
去重元素 | Flux.just(1,2,2,3).distinct() → 1、2、3 |
elementAt() |
取指定索引的元素(转 Mono) | Flux.range(1,5).elementAt(2) → Mono[3] |
2.1.4 组合类 API(多 Flux 合并)
| API 名称 | 作用 | 示例代码 |
|---|---|---|
concat() |
顺序合并(先完第一个,再执行第二个) | Flux.concat(Flux.just(1,2), Flux.just(3,4)) → 1、2、3、4 |
merge() |
并行合并(按元素产生顺序输出) | Flux.merge(Flux.interval(Duration.ofMillis(100)).take(2), Flux.interval(Duration.ofMillis(50)).take(2)) → 0(第二个)、0(第一个)、1(第二个)、1(第一个) |
zip() |
按位置配对合并(需同长度) | Flux.zip(Flux.just("A","B", "C"), Flux.just(1,2)) → (A,1)、(B,2) |
then() |
执行完当前 Flux 后执行另一个(忽略当前结果) | Flux.just(1,2).then(Flux.just("完成")) → Mono["完成"] |
2.1.5 错误处理 API(异常兜底)
| API 名称 | 作用 | 示例代码 |
|---|---|---|
onErrorReturn() |
出错时返回默认值 | Flux.error(new RuntimeException()).onErrorReturn("默认值") → "默认值" |
onErrorResume() |
出错时切换到备用 Flux | Flux.error(new RuntimeException()).onErrorResume(e -> Flux.just("备用1","备用2")) → "备用1"、"备用2" |
retry() |
出错时重试 N 次 | Flux.error(new RuntimeException()).retry(2) → 重试2次后仍报错 |
retryWhen() |
自定义重试策略(如延迟重试) | Flux.error(new RuntimeException()).retryWhen(Retry.fixedDelay(2, Duration.ofSeconds(1))) → 延迟1秒重试,共2次 |
2.1.6 生命周期 API
| API 名称 | 作用 | 示例代码 |
|---|---|---|
doOnNext() |
每个元素产生时执行(如日志) | Flux.range(1,3).doOnNext(num -> System.out.println("处理:" + num)) |
doOnComplete() |
流完成时执行 | Flux.range(1,3).doOnComplete(() -> System.out.println("完成")) |
doOnError() |
出错时执行 | Flux.error(new RuntimeException()).doOnError(e -> System.err.println("错误:" + e)) |
doOnSubscribe() |
订阅时执行 | Flux.just(1).doOnSubscribe(s -> System.out.println("订阅了")) |
2.2 Mono 常用 API
Mono 是"0/1 元素"的特殊 Flux,大部分 API 和 Flux 通用,以下是 Mono 特有/高频 API:
2.2.1 创建类 API
| API 名称 | 作用 | 示例代码 |
|---|---|---|
just() |
包含单个元素 | Mono.just("Hello") |
empty() |
空 Mono | Mono.empty() |
fromCallable() |
从异步任务创建 | Mono.fromCallable(() -> "异步结果") |
fromRunnable() |
从无返回值任务创建(转 Mono) | Mono.fromRunnable(() -> System.out.println("执行任务")) |
never() |
永不触发任何信号(无 onNext/onComplete) | Mono.never() |
2.2.2 转换/兜底 API(Mono 特有)
| API 名称 | 作用 | 示例代码 |
|---|---|---|
defaultIfEmpty() |
为空时返回默认值 | Mono.empty().defaultIfEmpty("默认值") → "默认值" |
switchIfEmpty() |
为空时切换到备用 Mono | Mono.empty().switchIfEmpty(Mono.just("备用值")) → "备用值" |
flatMapMany() |
转换为 Flux(一对多) | Mono.just("A").flatMapMany(s -> Flux.just(s + "1", s + "2")) → A1、A2 |
single() |
确保只有一个元素(否则报错) | Flux.just(1).single() → Mono[1](若 Flux 有多个元素则报错) |
2.2.3 组合类 API
| API 名称 | 作用 | 示例代码 |
|---|---|---|
zipWith() |
和另一个 Mono 配对 | Mono.just("A").zipWith(Mono.just(1)) → (A,1) |
thenReturn() |
执行完后返回指定值 | Mono.fromRunnable(() -> {}).thenReturn("完成") → "完成" |
when() |
等待多个 Mono 完成(转 Mono) | Mono.when(Mono.just(1), Mono.just(2)) → 所有完成后触发 onComplete() |
2.2.4 阻塞获取结果(测试/特殊场景)
| API 名称 | 作用 | 示例代码 |
|---|---|---|
block() |
阻塞获取结果(无则返回 null) | Mono.just("A").block() → "A" |
blockOptional() |
阻塞获取 Optional 结果(避免 null) | Mono.empty().blockOptional() → Optional.empty() |
2.3 通用高频 API(Flux/Mono 都适用)
| API 名称 | 作用 |
|---|---|
subscribe() |
订阅触发执行(核心!无订阅不执行) |
log() |
打印数据流生命周期日志(调试神器) |
delayElements() |
延迟每个元素的发射 |
timeout() |
设置超时时间(超时触发错误) |
cache() |
缓存结果(避免重复执行) |
2.4 高级 API
2.4.1 combineLatest
combineLatest 是 Flux/Mono 中用于多数据流组合的 API,核心作用是 : 当任意一个输入数据流产生新元素时,立即收集所有输入数据流的最新元素,并通过指定的组合函数生成一个新元素。
简单比喻:
假设有两个数据流 A 和 B,combineLatest 就像一个 "实时汇总器":
- A 产生元素 a1 → 无输出(B 还没元素);
- B 产生元素 b1 → 汇总 (a1, b1) 输出;
- A 产生元素 a2 → 汇总 (a2, b1) 输出;
- B 产生元素 b2 → 汇总 (a2, b2) 输出。
基础用法 :
java
public static void main(String[] args) throws InterruptedException {
// 数据流1:每100ms产生一个元素,共3个
Flux<Long> flux1 = Flux.interval(Duration.ofMillis(100))
.take(3)
.doOnNext(num -> System.out.println("flux1 产生:" + num));
// 数据流2:每150ms产生一个元素,共2个
Flux<Long> flux2 = Flux.interval(Duration.ofMillis(150))
.take(2)
.doOnNext(num -> System.out.println("flux2 产生:" + num));
// 组合两个流:任意流有新元素,就取两者最新值相加
Flux<Long> combined = Flux.combineLatest(
flux1,
flux2,
(num1, num2) -> num1 + "-"+ num2 // 组合函数:两个最新值拼接
);
// 订阅输出
/**
输出结果 :
flux1 产生:0
flux2 产生:0
组合结果:0-0
flux1 产生:1
组合结果:1-0
flux2 产生:1
flux1 产生:2
组合结果:1-1
组合结果:2-1
**/
combined.subscribe(result -> System.out.println("组合结果:" + result));
// 等待异步执行完成
Thread.sleep(5000);
}
使用场景举例
假设一个订单表单,需要实时计算 "数量 × 单价" 的总价,适合用 combineLatest,如下
java
// 数量数据流(用户输入)
Flux<Integer> quantityFlux = Flux.just(1, 2, 3);
// 单价数据流(用户输入/后端返回)
Flux<BigDecimal> priceFlux = Flux.just(new BigDecimal("10.5"), new BigDecimal("11.0"));
// 实时计算总价
Flux<BigDecimal> totalPriceFlux = Flux.combineLatest(
quantityFlux,
priceFlux,
(qty, price) -> price.multiply(new BigDecimal(qty))
);
// 订阅输出:10.5 → 21.0 → 31.5 → 33.0
totalPriceFlux.subscribe(total -> System.out.println("实时总价:" + total));
注意事项:
- 初始元素要求:只有当所有输入流都产生了至少一个元素后,combineLatest 才会开始输出组合结果;
- 流结束规则:当所有输入流都结束时,组合流才会结束;若有一个流无限(如 interval),则组合流也会无限;
- 背压处理:combineLatest 会处理背压,但需注意:如果某个流产生元素过快,可能会积压最新值(只保留最新的那个);
- 空流处理:若任意一个输入流是空流(empty()),则组合流也为空(因为无法满足 "所有流都有至少一个元素")。
- 与 zip 区别 :combineLatest 是 "取每个流的最新值",zip 是 "取每个流同位置值(如都是第一个位置的元素、都是第二个位置的元素)";
- **combineLatest ** :combineLatest 有多个重载方法,通过各个重载方法可以实现
-
指定 "等待所有流初始元素的超时时间",避免无限等待:
java// 示例:超时时间1秒,调度器用并行线程池 Flux<Long> combinedWithTimeout = Flux.combineLatest( Duration.ofSeconds(1), Schedulers.parallel(), Flux.never(), // 永不产生元素的流 Flux.just(1L), (a, b) -> a + b ); // 1秒后触发 TimeoutException,可通过 onErrorResume 兜底 combinedWithTimeout.onErrorResume(e -> Flux.just(-1L)) .subscribe(System.out::println); // 输出:-1 -
需要组合的流数量不固定(如从列表中动态获取):
java// 示例:动态组合3个流 List<Flux<Long>> fluxList = Arrays.asList( Flux.interval(Duration.ofMillis(100)).take(2), Flux.interval(Duration.ofMillis(150)).take(2), Flux.interval(Duration.ofMillis(200)).take(2) ); Flux<String> combined = Flux.combineLatest( fluxList, // 组合函数:将各流最新元素数组转为字符串 elements -> Arrays.stream(elements) .map(Object::toString) .collect(Collectors.joining(",")) ); // 输出:0,0,0 → 1,0,0 → 1,1,0 → 1,1,1
-
常见坑点与避坑方案
| 坑点 | 现象 | 避坑方案 |
|---|---|---|
| 无限等待初始元素 | 组合流一直无输出 | 1. 用 defaultIfEmpty 给空流兜底;2. 用带超时的重载方法;3. 检查流是否为 never() |
| 结果重复输出 | 无意义的重复计算结果 | 结合 distinctUntilChanged 过滤重复结果 |
| 内存泄漏(无限流) | 组合流永不结束,占用资源 | 1. 用 take() 限制无限流的元素数量;2. 用 timeout() 设置整体超时 |
背压异常(MissingBackpressureException) |
生产者速度远大于消费者 | 给快流添加 onBackpressureLatest()/onBackpressureBuffer() 背压策略 |
2.4.2 contextWrite
contextWrite 是 Reactor 中用于传递上下文(Context) 的方法,核心作用是:在数据流的执行链中,附加"键值对"形式的上下文数据,这些数据可以在数据流的任意环节(上游/下游)读取,且上下文是线程安全、可传递的(即使切换调度器/线程池,上下文也不会丢失)。
其目的是解决传统异步编程中"线程本地变量(ThreadLocal)"在多线程场景下失效的问题------Reactor 的 Context 是绑定到数据流而非线程,完美适配异步非阻塞、线程切换的场景。
下面给出一个基本示例(写入+读取上下文):
java
public class ContextWriteDemo {
public static void main(String[] args) {
Flux<String> flux = Flux.just("A", "B", "C")
// 1. 写入上下文:添加 traceId(链路追踪ID)和 userId
.contextWrite(Context.of("traceId", "trace-123", "userId", 1001))
// 2. 读取上下文:处理每个元素时获取上下文数据
.doOnNext(element -> {
// 读取上下文(Reactor 3.4+ 推荐用 currentContext())
Context context = Context.current();
String traceId = context.get("traceId");
Integer userId = context.get("userId");
System.out.printf("traceId: %s, userId: %d, 处理元素:%s%n",
traceId, userId, element);
})
// 3. 追加上下文(覆盖/新增键值对)
.contextWrite(ctx -> ctx.put("traceId", "trace-456").put("appName", "reactor-demo"));
flux.subscribe();
}
}
执行结果:
traceId: trace-456, userId: 1001, 处理元素:A
traceId: trace-456, userId: 1001, 处理元素:B
traceId: trace-456, userId: 1001, 处理元素:C
需要注意的是:
- 多次调用
contextWrite会合并上下文 :后调用的会覆盖前调用的同键值(如traceId被覆盖为trace-456); - 上下文读取需在数据流操作符内(如
doOnNext/map),不能在外部主线程读取; - 上下文是不可变 的:
contextWrite不会修改原 Context,而是返回新的 Context。
2.4.3 materialize
materialize 是将数据流的信号(Signal) 封装为 Signal 对象的方法,核心作用是:把数据流中的 onNext(元素)、onError(异常)、onComplete(完成)等信号,统一封装为 Signal 实例,让你能"看到"数据流的完整生命周期。
对应的反向方法是
dematerialize:将Signal对象还原为原始数据流。
核心价值在于:
- 调试:直观看到数据流的所有信号(包括异常、完成信号);
- 监控:捕获数据流的执行状态(如是否完成、是否出错);
- 自定义处理:统一处理不同类型的信号(元素/异常/完成)
简单示例如下:
java
public class MaterializeDemo {
public static void main(String[] args) {
Flux<Integer> originalFlux = Flux.just(1, 2)
.concatWith(Flux.error(new RuntimeException("模拟错误")))
.concatWith(Flux.just(3)); // 错误后不会执行
// 1. materialize:封装所有信号为 Signal 对象
Flux<Signal<Integer>> signalFlux = originalFlux.materialize();
// 2. 订阅处理 Signal
signalFlux.subscribe(signal -> {
switch (signal.getType()) {
case ON_NEXT:
System.out.println("元素信号:" + signal.get());
break;
case ON_ERROR:
System.err.println("错误信号:" + signal.getThrowable().getMessage());
break;
case ON_COMPLETE:
System.out.println("完成信号");
break;
default:
break;
}
});
}
}
执行结果:
元素信号:1
元素信号:2
错误信号:模拟错误
需要注意的是:
Signal包含三种核心类型:ON_NEXT(元素)、ON_ERROR(异常)、ON_COMPLETE(完成);- 数据流出错时,
materialize会捕获ON_ERROR信号,不会直接抛出异常; dematerialize可还原信号为原始数据流(如signalFlux.dematerialize()会重新抛出异常)。
2.4.4 metrics
Reactor 提供的 metrics 相关能力(核心是 Metrics 工具类 + metrics() 操作符),核心作用是: 收集数据流的执行指标(如元素数量、耗时、错误数),并暴露给监控系统(如 Micrometer/Prometheus),实现响应式数据流的可观测性。
Reactor 内置的核心指标如下:
| 指标名称 | 含义 |
|---|---|
reactor.processed |
处理的元素总数 |
reactor.duration |
数据流执行总耗时 |
reactor.errors |
发生的错误总数 |
reactor.subscribe |
订阅次数 |
简单示例如下:(结合 Micrometer 监控)
- 前置依赖(Maven):
xml
<!-- Reactor 监控核心依赖 -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.5.11</version>
</dependency>
<!-- Micrometer 核心(对接 Prometheus/Grafana) -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>1.12.0</version>
</dependency>
- 代码示例:
java
public class MetricsDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 初始化 Reactor 监控(绑定到 Micrometer)
reactor.core.publisher.Metrics.addTimerPublisher(
"user.query.timer", // 指标名称
Timer::start, // 计时器启动
Timer::stop // 计时器停止
);
// 2. 给数据流添加监控
Flux<Integer> userFlux = Flux.range(1, 100)
.delayElements(Duration.ofMillis(10)) // 模拟耗时
.subscribeOn(Schedulers.boundedElastic())
// 3. 启用 metrics:收集该数据流的指标
.name("user.query") // 指标标签:数据流名称
.tag("env", "dev") // 自定义标签:环境
.metrics(); // 开启监控
// 4. 订阅执行
userFlux.subscribe(
num -> {},
error -> {},
() -> {
// 打印监控指标(生产环境对接 Prometheus)
System.out.println("处理元素数:" + Metrics.counter("reactor.processed", "name", "user.query").count());
System.out.println("错误数:" + Metrics.counter("reactor.errors", "name", "user.query").count());
}
);
Thread.sleep(2000);
}
}
执行结果:
处理元素数:100.0
错误数:0.0
需要注意的是:
name()/tag():给指标添加自定义标签,用于区分不同的数据流(如不同接口、不同环境);metrics():必须在name()/tag()之后调用,否则标签不生效;- 生产环境:需将 Micrometer 对接 Prometheus + Grafana,实现指标可视化和告警。
3. 高级特性
3.1 背压
背压是响应式编程的核心概念,用于处理生产者速度 > 消费者速度的场景。
在异步非阻塞的响应式编程中,生产者(发布者,如 Flux/Mono)和消费者(订阅者)的处理速度往往不一致,如:
- 生产者生产数据的速度极快(比如每秒 1000 条);
- 消费者处理数据的速度很慢(比如每秒 10 条)。
如果没有背压,生产者不断生产的数据会积压在内存中,最终导致 OOM(内存溢出) 或系统卡顿。
3.1.1 核心特点
背压(Backpressure)是消费者向生产者反馈 "处理能力" 的机制------ 消费者可以告诉生产者:"我现在只能处理 N 个数据,请你只生产 / 发送 N 个,等我处理完再要更多"。
核心特点:
- 反向反馈:不是生产者推多少,消费者就接多少;而是消费者 "按需索取";
- 非阻塞:反馈过程是异步的,不会阻塞生产者 / 消费者;
- 标准化:Reactor 遵循 Reactive Streams 标准,背压逻辑被封装在 Subscription 接口中。
类似于 RabbitMQ 中的 baseQos 的作用,根据消费者的能力来控制服务端的推送,即按需索取。
3.1.2 实现原理
Reactive Streams 定义了 Subscription 接口,这是背压的核心载体:
java
public interface Subscription {
// 消费者告诉生产者:"我现在能处理 n 个数据,请发给我"
void request(long n);
// 消费者告诉生产者:"我不再需要数据了,停止生产"
void cancel();
}
通俗点说,背压的执行流程如下:
- 消费者订阅生产者(subscribe()),生产者返回 Subscription 对象给消费者;
- 消费者调用 subscription.request(5) → 告诉生产者:"先给我 5 个数据";
- 生产者发送 5 个数据,消费者处理完后,再调用 subscription.request(3) → "再给我 3 个";
- 若消费者处理不过来,可暂停调用 request(),生产者就会停止发送数据;
- 若消费者不想处理了,调用 subscription.cancel() → 生产者停止生产并清理资源。
Reactor 内置了多种背压策略,应对不同的 "生产者过快" 场景,核心策略如下:
| 策略名称 | 核心逻辑 | 适用场景 |
|---|---|---|
onBackpressureBuffer |
缓存生产者超出的元素(默认无界,需指定大小) | 消费者偶尔慢,可短暂缓存(如指定缓存1000个) |
onBackpressureDrop |
丢弃生产者超出的元素 | 非关键数据(如日志、监控指标,丢一点不影响) |
onBackpressureLatest |
只保留最新的元素,丢弃旧的积压元素 | 实时性优先(如实时监控、表单实时计算) |
onBackpressureError |
超出处理能力时抛出 MissingBackpressureException |
不允许丢数据,需快速失败告警 |
下面我们举个例子来说明,如下:
java
public class BackpressureDemo {
public static void main(String[] args) throws InterruptedException {
// 生产者:每秒产生1000个元素(速度极快)
Flux<Integer> fastProducer = Flux.range(1, 10000)
.delayElements(Duration.ofMillis(1)) // 每秒1000个
.publishOn(Schedulers.parallel()); // 异步生产
// 消费者:每秒只能处理10个元素(速度极慢)
fastProducer
// 配置背压策略:只保留最新的元素,丢弃旧积压
.onBackpressureLatest()
.subscribe(
num -> {
// 模拟慢处理(100ms/个 → 每秒10个)
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
System.out.println("处理元素:" + num);
},
error -> System.err.println("错误:" + error)
);
// 等待执行完成
Thread.sleep(5000);
}
}
如果需要精细化控制数据接收速度(比如 "处理完 1 个再要 1 个"),可以手动控制 request 的调用时机,如下:
java
public static void main(String[] args) throws InterruptedException {
Flux<Integer> producer = Flux.range(1, 5)
.doOnNext(num -> System.out.println("生产者发送:" + num));
// 手动订阅,控制 request
producer.subscribe(new org.reactivestreams.Subscriber<Integer>() {
private Subscription subscription; // 保存订阅关系
// 1. 订阅成功后,回调 onSubscribe
@Override
public void onSubscribe(Subscription s) {
this.subscription = s;
// 第一步:先请求1个数据
System.out.println("消费者请求1个数据");
s.request(1);
}
// 2. 收到元素后处理,处理完再请求下一个
@Override
public void onNext(Integer num) {
System.out.println("消费者处理:" + num);
// 模拟耗时处理(1秒)
try { Thread.sleep(1000); } catch (InterruptedException e) {}
// 处理完当前元素,再请求1个
System.out.println("消费者再请求1个数据");
subscription.request(1);
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("所有数据处理完成");
}
});
// 等待执行完成
Thread.sleep(6000);
}
需要注意的是:
- 默认情况下,消费者订阅后,Reactor 会自动调用
subscription.request(Long.MAX_VALUE)→ 表示 "我能处理无限个元素"; - 当使用
publishOn()/subscribeOn()切换线程池时,Reactor 会根据线程池的队列大小自动调整 request() 的数量; - 只有当生产者速度远超消费者时,才需要手动指定背压策略(如
onBackpressureLatest)。
3.2 调度器
Reactor 是异步非阻塞编程模型,但默认情况下,数据流的执行线程是 "调用线程" ------ 比如在主线程中订阅 Flux,默认所有操作(生产、处理、消费)都在主线程执行,无法利用多核 CPU,也无法实现真正的 "异步非阻塞"。如下:
java
// 无调度器:所有操作都在主线程执行,耗时操作会阻塞主线程
Flux.range(1, 3)
.doOnNext(num -> System.out.println("处理线程:" + Thread.currentThread().getName()))
.map(num -> {
Thread.sleep(1000); // 模拟耗时操作
return num * 2;
})
.subscribe();
// 输出(所有操作都在 main 线程):
// 处理线程:main
// 处理线程:main
// 处理线程:main
调度器的核心价值是 控制 Reactor 数据流在哪个线程/线程池上执行,实现线程切换、异步执行、并行处理,充分利用多核CPU资源。
调度器(Scheduler)是 Reactor 中线程池的抽象,它封装了线程的创建、管理、复用逻辑,核心作用是:
- 指定数据流的生产线程 (
subscribeOn); - 指定数据流的消费/处理线程 (
publishOn); - 提供不同类型的线程池,适配不同的业务场景。
3.2.1 内置的核心调度器
Reactor 提供了 Schedulers 工具类,内置5种常用调度器,具体如下:
boundedElastic(重点):
- 为 IO 密集型任务设计(如调用第三方接口、数据库查询);
- 线程数动态扩展(但有上限,默认 CPU 核数×10),避免线程数爆炸;
- 空闲线程会自动回收(默认60秒),节省资源。
parallel:
- 为 CPU 密集型任务设计(如大数据计算、加密解密);
- 线程数固定为 CPU 核数,避免上下文切换开销,最大化CPU利用率。
| 调度器类型 | 核心特点 | 线程池类型 | 适用场景 |
|---|---|---|---|
Schedulers.immediate() |
无线程切换,在当前线程执行 | 无(复用当前线程) | 无需异步的简单操作 |
Schedulers.single() |
单线程池,所有任务在同一个线程执行 | 单线程池 | 串行执行、有状态的操作(如顺序写文件) |
Schedulers.elastic() |
弹性线程池(已过时,推荐 boundedElastic) | 动态线程池 | 已废弃,用 boundedElastic 替代 |
Schedulers.boundedElastic() |
有界弹性线程池(核心) | 动态线程池(最大数=CPU核数×10) | IO 密集型操作(数据库、网络请求、文件IO) |
Schedulers.parallel() |
并行线程池 | 固定线程池(线程数=CPU核数) | CPU 密集型操作(计算、排序、编码) |
2.3.2 使用示例
调度器最核心的两个 API是 subscribeOn 和 publishOn,如下:
subscribeOn:指定数据流的生产线程 (源头线程),从数据流源头开始生效(全局) 如 :Flux.just(1).subscribeOn(Schedulers.parallel())publishOn:指定数据流的消费/处理线程 (下游线程),从当前操作之后的所有操作生效(局部)Flux.just(1).publishOn(Schedulers.boundedElastic())
我们以下面的 Demo 为例:
java
public class SchedulerDemo {
public static void main(String[] args) throws InterruptedException {
Flux.range(1, 2)
// 1. subscribeOn:指定源头(生产)线程(parallel 线程池),即 Flux.range(1, 2) 这个操作会在 parallel 线程中完成
// 本例中 Flux.range(1, 2) 并不耗时,但在实际的一些场景中,Flux 的创建操作可能会执行耗时操作,此时就可以通过 subscribeOn 指定源头线程
.subscribeOn(Schedulers.parallel())
.doOnNext(num -> System.out.printf("subscribeOn 后:%s, 元素:%d%n",
Thread.currentThread().getName(), num))
// 2. publishOn:指定下游(处理)线程(boundedElastic 线程池)
.publishOn(Schedulers.boundedElastic())
.doOnNext(num -> System.out.printf("publishOn 后:%s, 元素:%d%n",
Thread.currentThread().getName(), num))
// 3. 再次 publishOn:切换到 single 线程池
.publishOn(Schedulers.single())
.doOnNext(num -> System.out.printf("第二次 publishOn 后:%s, 元素:%d%n",
Thread.currentThread().getName(), num))
.subscribe();
// 等待异步执行完成
Thread.sleep(1000);
}
}
执行结果如下,可以清晰看到线程切换:
subscribeOn 后:parallel-1, 元素:1
subscribeOn 后:parallel-1, 元素:2
publishOn 后:boundedElastic-1, 元素:1
publishOn 后:boundedElastic-1, 元素:2
第二次 publishOn 后:single-1, 元素:1
第二次 publishOn 后:single-1, 元素:2
需要注意的是:
subscribeOn无论放在数据流的哪个位置,都只影响源头的生产线程 (全局生效,且多个subscribeOn只有第一个生效);publishOn影响自身之后的所有操作(局部生效,可多次调用切换线程);- 多个
subscribeOn只有第一个生效,后续的会被忽略; - 实际开发中,通常的组合是:
subscribeOn(Schedulers.boundedElastic()):源头 IO 操作异步执行;publishOn(Schedulers.parallel()):下游 CPU 密集操作并行执行。
2.3.3 进阶用法
-
如果内置调度器不满足需求(如指定线程名称、自定义线程池参数),可自定义:
java// 自定义线程池(核心线程数2,最大线程数4) Scheduler customScheduler = Schedulers.fromExecutor( Executors.newThreadPoolExecutor( 2, // 核心线程数 4, // 最大线程数 60, // 空闲线程回收时间 java.util.concurrent.TimeUnit.SECONDS, new java.util.concurrent.LinkedBlockingQueue<>() ), "custom-thread" // 线程名称前缀 ); // 使用自定义调度器 Flux.just(1,2) .publishOn(customScheduler) .doOnNext(num -> System.out.println("自定义线程:" + Thread.currentThread().getName())) .subscribe(); -
调度器的资源释放 : 调度器的线程池是后台线程,若程序未正常退出,需手动释放资源:
javaScheduler scheduler = Schedulers.boundedElastic(); try { Flux.just(1).publishOn(scheduler).subscribe(); } finally { scheduler.dispose(); // 释放线程池资源 }
四、扩展
1. Reactor 与 RxJava 的关系
RxJava(2.x+)和 Reactor 都遵循 Reactive Streams 标准(JSR-310),两者都是响应式编程库:
- 都支持异步数据流、背压控制;
- 核心思想都是"数据流的异步处理"。
核心区别 如下:
| 特性 | RxJava | Reactor |
|---|---|---|
| 开发团队 | Netflix(后移交社区) | Pivotal(Spring 母公司) |
| 生态关联 | 通用型,无强绑定生态 | 深度绑定 Spring 生态(Spring 官方推荐) |
| 核心类型 | Observable/Flowable/Single | Flux/Mono(更简洁,适配 Java 8) |
| 目标场景 | 多平台(Java/Android) | 后端 Java 开发(尤其是 Spring 项目) |
| 学习曲线 | 操作符极多(数百个),稍复杂 | 操作符精简,贴合 Spring 开发者习惯 |
Reactor 提供了和 RxJava 互转的工具(reactor-adapter),可以在项目中混合使用:
java
// Reactor Flux 转 RxJava Flowable
Flux<String> flux = Flux.just("a", "b", "c");
Flowable<String> flowable = FluxAdapter.fluxToFlowable(flux);
// RxJava Flowable 转 Reactor Flux
Flowable<String> flowable = Flowable.just("x", "y", "z");
Flux<String> flux = FlowableAdapter.flowableToFlux(flowable);
总的来说 :
- Reactor 核心 :基于 Reactive Streams 的 Java 8+ 响应式库,核心是
Mono(0/1 元素)和Flux(0/N 元素),主打异步非阻塞、背压控制; - 核心优势:相比传统同步编程,能更高效利用线程资源,支持背压,高并发场景下响应更稳定;
- 与 RxJava 的关系:两者同属响应式编程库(遵循 Reactive Streams),RxJava 更通用,Reactor 是 Spring 生态的首选,且两者可互相转换。
简单来说,如果你是 Spring 开发者,优先用 Reactor;如果是 Android 或非 Spring 项目,RxJava 是更通用的选择。
2. Reactor 与 I/O 多路复用
2.1 概念说明
- 多路复用(I/O Multiplexing) :是操作系统层面 的 I/O 处理机制,核心是"一个线程监听多个 I/O 通道(如网络套接字),当某个通道有数据就绪时,再去处理这个通道的 I/O",典型实现有 Linux 的
epoll、BSD 的kqueue、Windows 的IOCP。 - Reactor 响应式编程 :是应用层 的编程模型,基于"异步非阻塞 + 事件驱动",核心是通过
Mono/Flux处理数据流,解决传统同步阻塞编程的线程浪费问题。 这里要注意一个易混点:- 操作系统层面有一个 Reactor 设计模式 (也叫"反应器模式"),它是多路复用的一种应用模式:用一个线程监听 I/O 事件(多路复用),事件就绪后分发给工作线程处理;
- 而我们说的 Reactor 响应式编程库,是基于"Reactor 设计模式 + Reactive Streams 标准"封装的应用层库。
两者都是为了解决同步阻塞 I/O 的低效问题:
- 多路复用:解决"一个线程只能处理一个 I/O"的底层限制,让少量线程能监听大量 I/O 事件;
- Reactor 响应式编程:解决"应用层一个请求占用一个线程"的问题,基于底层的异步 I/O 机制,实现应用层的高效并发。
简单比喻:
- 多路复用是"工厂的智能监控系统",能同时监控多条生产线的状态,只有生产线有物料就绪时才通知工人;
- Reactor 是"工厂的工人工作模式",工人不再守着一条生产线等,而是根据监控系统的通知,灵活处理有就绪任务的生产线。
2.2 核心区别
二者的核心区别在 层面和范围不同
| 维度 | 多路复用 | Reactor 响应式编程 |
|---|---|---|
| 所属层面 | 操作系统内核/底层 I/O 层 | 应用层编程模型 |
| 处理对象 | I/O 事件(如网络套接字就绪、文件可读) | 业务数据流(如接口请求、数据库结果) |
| 核心机制 | 监听多个 I/O 通道,事件就绪后触发 | 异步数据流处理 + 背压控制 + 操作符链 |
| 关注范围 | 仅聚焦 I/O 事件的高效监听 | 全链路的异步编程(I/O + 业务逻辑) |
| 典型实现 | epoll、kqueue、IOCP | Reactor 库(Mono/Flux)、Spring WebFlux |
2.3 实际关联
Reactor 本身不实现多路复用,但它的异步非阻塞能力依赖底层的多路复用机制:
- 比如在 Spring WebFlux(基于 Reactor)中,默认使用 Netty 作为服务器,而 Netty 底层正是基于
epoll/kqueue等多路复用技术实现的异步 I/O; - 没有多路复用,Reactor 的异步非阻塞就失去了底层支撑,最终还是会退化为"伪异步"(比如用线程池模拟异步)。
举个实际执行流程:
客户端请求 → Netty(底层 epoll 多路复用)监听请求就绪 → Reactor(Mono/Flux)异步处理请求 → 调用数据库异步驱动(底层也是多路复用)→ 返回响应
- 多路复用:负责"监听请求/数据库 I/O 是否就绪";
- Reactor:负责"把就绪后的 I/O 结果封装成数据流,异步处理业务逻辑"。
2.4 总结
- 核心结论 :Reactor 响应式编程和多路复用不相同,但高度关联------多路复用是底层 I/O 高效处理机制,Reactor 是应用层基于异步 I/O 的编程模型,Reactor 依赖多路复用实现真正的异步非阻塞;
- 关键区别:多路复用聚焦"底层 I/O 事件监听",Reactor 聚焦"应用层数据流的异步处理"(还包含背压、操作符等多路复用没有的特性);
- 易混点:操作系统的 Reactor 模式是多路复用的应用,而 Reactor 响应式库是对该模式的上层封装。
简单来说,可以理解为:多路复用是"地基",Reactor 响应式编程是"地基上的楼房"------楼房的高效运作依赖地基,但楼房本身还有自己的结构和功能(比如背压、数据流操作),这是地基没有的。
五、参考内容
- 豆包