Flux 与 Mono:Project Reactor 核心响应式类型深度解析

Project Reactor 是Java响应式编程库 ,提供MonoFlux 核心类型,支持非阻塞、背压及异步数据流处理 。它是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 中程序化发射数据的现代方案

引言

在 Project Reactor 的架构体系中,FluxMono 是两大核心响应式类型(Reactive Types),它们共同构成了 Reactor 响应式编程模型的基石。作为 Reactive Streams 规范中 Publisher 接口的具体实现,二者承载了从数据源创建、操作符变换、调度器切换到背压控制的全生命周期管理。 理解 Flux 与 Mono 的语义、行为与协作方式,是掌握 Reactor 乃至整个 Spring WebFlux 技术栈的第一步。

一、Reactive Streams 规范回顾

在深入 Flux 与 Mono 之前,有必要回顾 Reactive Streams 规范定义的四个核心接口:

java 复制代码
public interface Publisher<T> {
    void subscribe(Subscriber<? super T> subscriber);
}

public interface Subscriber<T> {
    void onSubscribe(Subscription s);
    void onNext(T t);
    void onError(Throwable t);
    void onComplete();
}

public interface Subscription {
    void request(long n);
    void cancel();
}

public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {}

信号协议(Signal Protocol):

文本 复制代码
onSubscribe → onNext × (0..N) → [onComplete | onError]
  • onSubscribe:订阅建立时首先触发,传递 Subscription 对象
  • onNext:推送数据元素,可触发 0 到 N 次
  • onComplete:正常终止信号,此后不再有任何信号
  • onError:错误终止信号,此后不再有任何信号
  • onComplete 与 onError 互斥,且最多出现一次

Flux 与 Mono 正是对这一协议的两种具体语义化实现。

二、Flux:0 到 N 个元素的异步序列

2.1 定义与语义

FluxT 是一个 Reactive Streams Publisher,可以发射 0 到 N 个元素,然后正常完成(onComplete)或以错误终止(onError)。

文本 复制代码
Flux.just("A", "B", "C") 的 Marble 图:

  ──A──B──C──|>
              onComplete

Flux.error(new RuntimeException()) 的 Marble 图:

  ──X>
     onError

Flux 适用于表示:

  • 集合数据的响应式遍历(如数据库查询结果集)
  • 事件流(如 WebSocket 消息、SSE 推送)
  • 定时序列(如 Flux.interval())
  • 无限流(如传感器数据流、日志流)

2.2 创建 Flux 的方式

Reactor 提供了丰富的静态工厂方法: (1)从已知值创建

java 复制代码
// 固定元素
Flux<Integer> numbers = Flux.just(1, 2, 3, 4, 5);

// 空 Flux
Flux<String> empty = Flux.empty();

// 错误 Flux
Flux<Object> error = Flux.error(new RuntimeException("boom"));

(2)从集合/数组创建

java 复制代码
List<String> list = List.of("alpha", "beta", "gamma");
Flux<String> fromList = Flux.fromIterable(list);

String[] arr = {"x", "y", "z"};
Flux<String> fromArray = Flux.fromArray(arr);

// 从 Java Stream 创建(注意:只能消费一次)
Flux<String> fromStream = Flux.fromStream(list.stream());

(3)程序化生成

java 复制代码
// range: 生成整数区间
Flux<Integer> range = Flux.range(1, 100);

// interval: 定时发射(无限流)
Flux<Long> ticks = Flux.interval(Duration.ofMillis(500));

// generate: 同步、逐个生成(有状态)
Flux<Integer> generated = Flux.generate(
    () -> 0,                                    // 初始状态
    (state, sink) -> {
        sink.next(state);                       // 发射当前值
        if (state == 10) sink.complete();       // 终止条件
        return state + 1;                       // 返回新状态
    }
);

// create: 异步、多线程生成(桥接回调 API)
Flux<String> created = Flux.create(sink -> {
    messageListener.onMessage(msg -> sink.next(msg.getBody()));
    messageListener.onError(sink::error);
    messageListener.onComplete(sink::complete);
    sink.onDispose(() -> messageListener.close());
});

// push: 异步、单线程生成
Flux<String> pushed = Flux.push(sink -> {
    singleThreadedEmitter.register(sink::next);
});

(4)惰性创建

java 复制代码
// defer: 每次 subscribe 时才决定创建哪个 Publisher
Flux<Long> deferred = Flux.defer(() -> 
    Flux.just(System.currentTimeMillis())
);

2.3 Flux 的三种终止形态

形态 描述 示例
有限正常完成 发射 N 个元素后发出 onComplete Flux.just(1,2,3)
错误终止 发射 0~N 个元素后发出 onError Flux.error(...)
无限流 永不发出终止信号 Flux.interval(...)

⚠️ 无限流必须配合 take(n)、timeout() 或手动 cancel() 使用,否则永远不会结束。

三、Mono:0 或 1 个元素的异步结果

3.1 定义与语义

MonoT 是一个 Reactive Streams Publisher,最多发射 0 或 1 个元素,然后正常完成或以错误终止。

文本 复制代码
Mono.just("Hello") 的 Marble 图:

  ──Hello──|>
            onComplete

Mono.empty() 的 Marble 图:

  ──|>
    onComplete(无元素)

Mono 的语义等价于:

  • Optional + CompletableFuture 的响应式增强版
  • 表示一个异步的"可能有值、可能为空、可能失败"的计算结果

适用场景:

  • 数据库 findById 查询
  • HTTP 请求获取单个资源
  • 写操作(save/delete)的结果确认
  • 任何"最多返回一个值"的异步操作

3.2 创建 Mono 的方式

java 复制代码
// 有值
Mono<String> just = Mono.just("Hello Reactor");

// 空值
Mono<Void> empty = Mono.empty();

// 错误
Mono<Object> error = Mono.error(new IllegalArgumentException("bad"));

// 惰性
Mono<Long> deferred = Mono.defer(() -> Mono.just(System.currentTimeMillis()));

// 从 Callable 桥接阻塞调用
Mono<String> fromCallable = Mono.fromCallable(() -> blockingService.fetch());

// 从 CompletableFuture 桥接
Mono<User> fromFuture = Mono.fromFuture(asyncService.findUser(1L));

// 从 Publisher 桥接
Mono<User> fromPublisher = Mono.from(somePublisher);

3.3 Mono 特有的操作

java 复制代码
Mono<User> userMono = userRepository.findById(1L);

// 如果为空则使用默认值
User user = userMono.block();  // ⚠️ 仅在测试/启动时允许

// 如果为空则切换到另一个 Mono
Mono<User> withFallback = userMono.switchIfEmpty(defaultUserMono);

// 将 Mono<T> 转为 Mono<Void>(忽略值,只关心完成信号)
Mono<Void> ignored = userMono.then();

// 在值到达后执行副作用,但不改变流
Mono<User> withLog = userMono.doOnNext(u -> log.info("Got: {}", u));

// 延迟订阅
Mono<User> delayed = Mono.delay(Duration.ofSeconds(3))
                         .then(userMono);

四、Flux ↔ Mono 互转

两种类型之间可以灵活转换:

java 复制代码
// Flux → Mono
Mono<Integer> first = Flux.just(1, 2, 3).next();           // 取第一个
Mono<Integer> last  = Flux.just(1, 2, 3).last();           // 取最后一个
Mono<Integer> single = Flux.just(42).single();              // 确保恰好一个,否则报错
Mono<List<Integer>> list = Flux.just(1,2,3).collectList();  // 聚合为 List
Mono<Integer> sum = Flux.just(1,2,3).reduce(0, Integer::sum); // 聚合为单值

// Mono → Flux
Flux<Integer> flux = Mono.just(42).flux();                 // 0或1个元素的 Flux

// Mono → 多元素 Flux(flatMapMany)
Flux<Order> orders = userMono.flatMapMany(user -> 
    orderRepository.findByUserId(user.getId())
);

五、Subscribe:一切执行的起点

5.1 惰性求值原则

Reactor 中最核心的原则之一:

在调用 subscribe() 之前,操作符链仅仅是声明式的"蓝图",不会有任何数据流动或副作用发生。

java 复制代码
// ❗ 这行代码什么都不会发生
Flux<String> lazy = Flux.just("A", "B", "C")
        .map(s -> {
            System.out.println("Processing: " + s);  // 不会打印
            return s.toLowerCase();
        });

// ✅ 必须 subscribe 才触发执行
lazy.subscribe(System.out::println);

5.2 subscribe 的重载形式

java 复制代码
// 1. 最简形式:只消费数据
flux.subscribe(data -> System.out.println(data));

// 2. 数据 + 错误处理
flux.subscribe(
    data  -> System.out.println(data),
    error -> log.error("Stream error", error)
);

// 3. 数据 + 错误 + 完成
flux.subscribe(
    data    -> System.out.println(data),
    error   -> log.error("Stream error", error),
    ()      -> System.out.println("Stream completed")
);

// 4. 数据 + 错误 + 完成 + 订阅回调
flux.subscribe(
    data         -> System.out.println(data),
    error        -> log.error("Stream error", error),
    ()           -> System.out.println("Done"),
    subscription -> subscription.request(10)  // 背压:只请求10个
);

5.3 内部执行流程

当 subscribe() 被调用时,内部发生以下流程:

文本 复制代码
Subscriber.subscribe(Publisher)
    → Publisher 内部创建 Operator 链
    → 从最下游操作符向上游逐层调用 subscribe()
    → 到达源头 Publisher
    → 源头调用 Subscriber.onSubscribe(Subscription)
    → Subscriber 通过 Subscription.request(n) 请求数据
    → 数据沿操作符链从上游向下游流动(onNext)
    → 最终到达终端 Subscriber

这是一个 "订阅信号向上游传播,数据信号向下游流动" 的双向过程。

六、操作符:装配线上的工作站

6.1 操作符的本质

每个操作符接收一个上游 Publisher,返回一个新的 Publisher 实例,并在其中包装了特定的处理逻辑。

java 复制代码
Flux.just(1, 2, 3, 4, 5)     // Publisher A
    .filter(i -> i % 2 == 0)  // 返回新的 Publisher B(包装了 filter 逻辑)
    .map(i -> i * 10)          // 返回新的 Publisher C(包装了 map 逻辑)
    .subscribe();              // 订阅 C → 触发 B → 触发 A

⚠️ 常见误解:操作符不会修改原始 Publisher,而是创建新的包装实例。如果你"忘记"使用某个操作符的返回值,那个操作符就不会生效。

6.2 核心操作符分类

类别 操作符 说明
转换 map, flatMap, concatMap, flatMapSequential 元素变换
过滤 filter, distinct, skip, take, takeUntil 筛选元素
组合 merge, concat, zip, combineLatest 多流合一
聚合 reduce, scan, collectList, count 多值→单值
窗口 buffer, window, bufferTimeout, windowTimeout 分批处理
错误 onErrorResume, onErrorReturn, retry, retryWhen 异常恢复
调度 subscribeOn, publishOn 线程切换
调试 log, doOnNext, doOnError, doOnComplete 生命周期观察

6.3 map vs flatMap:最核心的区别

java 复制代码
// map: 同步 1:1 转换
Flux<String> upper = Flux.just("a", "b", "c")
        .map(String::toUpperCase);  // A, B, C

// flatMap: 异步 1:N 转换(每个元素映射为一个 Publisher)
Flux<Order> orders = Flux.just(userId1, userId2, userId3)
        .flatMap(id -> orderService.findByUserId(id));  // 并发执行,不保序

// concatMap: 异步 1:N 转换,保序
Flux<Order> orderedResults = Flux.just(userId1, userId2, userId3)
        .concatMap(id -> orderService.findByUserId(id));  // 顺序执行

七、背压:Flux 独有的挑战

7.1 为什么 Mono 不需要担心背压?

Mono 最多只有一个元素,不存在"生产速度超过消费速度"的问题。而 Flux 可能发射大量甚至无限个元素,背压是 Flux 必须面对的核心问题

7.2 背压策略

java 复制代码
Flux<Long> fastProducer = Flux.interval(Duration.ofMillis(1));

// 策略1:缓冲(默认无界,可设上限)
fastProducer.onBackpressureBuffer(1024);

// 策略2:丢弃
fastProducer.onBackpressureDrop();

// 策略3:只保留最新
fastProducer.onBackpressureLatest();

// 策略4:溢出报错
fastProducer.onBackpressureError();

7.3 消费者控制速率

java 复制代码
Flux.range(1, 1_000_000)
    .subscribe(new BaseSubscriber<Integer>() {
        @Override
        protected void hookOnSubscribe(Subscription subscription) {
            request(10);  // 初始只请求 10 个
        }

        @Override
        protected void hookOnNext(Integer value) {
            slowProcess(value);
            request(1);   // 每处理完一个,再要一个
        }
    });

7.4 limitRate:操作符级预取控制

java 复制代码
Flux.range(1, 1000)
    .limitRate(10)   // 每次最多向上游请求 10 个元素
    .subscribe();

八、Hot vs Cold:两种 Publisher 模式

特性 Cold Publisher Hot Publisher
数据起点 每个订阅者从头开始 从订阅时刻开始(可能错过之前的数据)
典型代表 Flux.just(), HTTP 调用 Flux.create() 的实时事件源
类比 点播视频(从头播放) 电视直播(进来看到什么算什么)
复用 每次订阅独立执行 所有订阅者共享同一数据源
java 复制代码
// Cold: 每次 subscribe 都重新执行
Flux<Long> cold = Flux.just(System.currentTimeMillis());

// 变为 Hot(多播)
ConnectableFlux<Long> hot = Flux.interval(Duration.ofSeconds(1)).publish();
hot.connect();  // 开始发射
hot.subscribe(v -> System.out.println("S1: " + v));
Thread.sleep(3000);
hot.subscribe(v -> System.out.println("S2: " + v));  // S2 错过了前 3 个

九、与 Spring WebFlux 的集成

在 Spring WebFlux 中,Controller 方法返回的 Mono / Flux 会由框架自动订阅:

java 复制代码
@RestController
public class UserController {

    // Mono: 返回单个对象或 404
    @GetMapping("/users/{id}")
    public Mono<ResponseEntity<User>> getUser(@PathVariable Long id) {
        return userRepository.findById(id)
                .map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    // Flux: 返回列表
    @GetMapping("/users")
    public Flux<User> listUsers() {
        return userRepository.findAll();
    }

    // Flux + SSE: 服务端推送
    @GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> streamEvents() {
        return eventService.listen()
                .map(e -> ServerSentEvent.<String>builder()
                        .data(e.getMessage())
                        .build());
    }
}

十、测试:StepVerifier

Reactor 提供了 reactor-test 模块中的 StepVerifier 用于精确验证流的行为:

java 复制代码
@Test
void testFluxBehavior() {
    StepVerifier.create(
        Flux.just(1, 2, 3).map(i -> i * 10)
    )
    .expectNext(10)
    .expectNext(20)
    .expectNext(30)
    .verifyComplete();
}

@Test
void testFluxError() {
    StepVerifier.create(
        Flux.just(1, 2, 0).map(i -> 10 / i)
    )
    .expectNext(10)
    .expectNext(5)
    .expectError(ArithmeticException.class)
    .verify();
}

@Test
void testWithVirtualTime() {
    StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofHours(1)).take(3))
        .thenAwait(Duration.ofHours(1))
        .expectNext(0L)
        .thenAwait(Duration.ofHours(1))
        .expectNext(1L)
        .thenAwait(Duration.ofHours(1))
        .expectNext(2L)
        .verifyComplete();
}

十一、最佳实践总结

# 实践 说明
1 不要 block() 在响应式链中调用 block() 会阻塞线程,破坏非阻塞模型
2 不要在 Service 层 subscribe 返回 Mono/Flux 给上层,让框架或最终消费者决定订阅时机
3 阻塞代码用 boundedElastic 包装 Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())
4 flatMap 中隔离错误 对每个子 Publisher 添加 onErrorResume,避免一个失败终止整个流
5 无限流必须设终止条件 take(n) / timeout() / takeUntil()
6 合理使用 limitRate 控制预取量,避免内存溢出
7 用 defer 包装副作用 确保每次订阅都重新计算
8 Context 替代 ThreadLocal 响应式链中 ThreadLocal 不可靠,使用 Reactor Context

十二、总结

文本 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Reactive Streams                         │
│                    Publisher<T>                             │
│                    ┌────────────┐                           │
│                    │            │                           │
│              ┌─────┴────┐  ┌───┴────┐                       │
│              │  Flux<T> │  │ Mono<T>│                       │
│              │  0..N    │  │  0..1  │                       │
│              └──────────┘  └────────┘                       │
│                                                             │
│  创建 → 转换 → 过滤 → 组合 → 聚合 → 调度 → 错误处理 → 订阅        │
│                                                             │
│  核心原则:                                                   │
│  • 惰性求值:subscribe 才执行                                  │
│  • 声明式:描述"做什么"而非"怎么做"                              │
│  • 背压:下游控制上游速率                                       │
│  • 不可变:操作符返回新实例                                     │
└─────────────────────────────────────────────────────────────┘

Flux 与 Mono 是 Project Reactor 的核心抽象,它们将异步数据流统一为可组合、可变换、可背压的声明式管道。掌握这两种类型的语义与操作方式,是构建高性能响应式应用的必经之路。

相关推荐
lingran__1 小时前
C++ STL unordered系列(哈希) 底层剖析与模拟实现万字详解 | 基于哈希表,复刻 SGI-STL 泛型哈希容器架构
开发语言·c++·后端·哈希算法·哈希表·泛型编程·unordered系列
小林ixn1 小时前
NestJS 入门实战:从 0 到 1 撸一个 Todo CRUD,感受装饰器与模块化的优雅
后端·mvc·nestjs
阿弱1 小时前
graph-core 的边与命令模式设计
java·后端·agent
智驭未来掌门人1 小时前
利用Qt设计实现一款桌面程序
后端
长栎1 小时前
你以为抽象工厂是「创建一组对象」——其实它是「锁定产品族兼容性」
后端
长栎1 小时前
你的 AI 品控规则越来越多了——但它们已经在打架了,你没看见
后端
有脚就行1 小时前
第24篇-Go-gRPC推理服务-高性能跨语言通信
开发语言·人工智能·后端·golang
不好听6131 小时前
NestJS 是什么?一张图看懂企业级后端的骨架
后端·nestjs
liuxiaocheng1 小时前
文本生成的进阶:generateText / streamText 里迟早会撞上的东西
前端·后端·ai编程