一文搞懂 Flux 和 Mono 底层原理
Flux 和 Mono 是 Project Reactor(Spring WebFlux 的底层响应式库)的两大核心类型,它们都实现了 Reactive Streams 规范 中的 Publisher<T> 接口,本质上是一种 惰性、声明式、基于发布-订阅模型的异步数据流抽象。
一、Flux 与 Mono 是什么?
| 类型 | 元素数量 | 语义类比 | 典型场景 |
|---|---|---|---|
Flux<T> |
0 ~ N | 异步版 Iterable<T> |
列表查询、事件流、WebSocket、SSE |
Mono<T> |
0 或 1 | 异步版 Optional<T> / CompletableFuture<T> |
单条查询、HTTP 调用、保存结果、完成信号 |
两者都是抽象类,所有操作符(map、filter、flatMap 等)都返回新的 Flux/Mono 实例,因此可以无限链式组合。
二、理论基石:Reactive Streams 规范
Reactor 的所有实现完全对齐 Reactive Streams 规范,该规范定义了四个核心接口:
Publisher<T>:数据发布者(生产者),提供subscribe(Subscriber)方法。Subscriber<T>:数据订阅者(消费者),接收onSubscribe、onNext、onError、onComplete四种信号。Subscription:订阅句柄,提供request(n)(请求 n 个数据)和cancel()(取消订阅)。Processor<T,R>:兼具 Publisher 和 Subscriber 能力,用于中间处理。
信号协议为:
onSubscribe → onNext × (0..N) → [onComplete | onError]
onComplete 和 onError 互斥,最多出现一次,之后不再有任何信号。
三、核心运行机制:订阅 → 请求 → 推送
1. 惰性构建:不订阅,不干活
调用 Flux.just()、Mono.fromCallable() 等操作符时,不会立即执行任何操作,只是组装出一条算子责任链。甚至连 Flux.interval() 这种定时器,在没人订阅时根本不会开始计时。
2. subscribe() 触发反向订阅链
调用 subscribe() 后,订阅从下游向上游反向建立连接:
Subscriber ← take ← filter ← map ← 数据源(FluxArray)
每个操作符都会包装传入的 Subscriber,创建自己的内部 Subscriber,然后向上游订阅。最终数据源收到被层层包装后的 Subscriber,开始发送数据。
3. onSubscribe:交出控制权
数据源通过 onSubscribe(Subscription) 将 Subscription 对象传递给下游,下游拿到控制权后,可以决定何时、请求多少数据。
4. request(n):背压的核心入口
下游调用 request(n) 向上游声明"我能处理 n 个数据",上游据此推送对应数量的 onNext。只要不调用 request(),数据就永远停在上游。
5. 终止信号
数据发完触发 onComplete(),出错触发 onError(),同时自动销毁 Subscription、释放资源。
四、背压(Backpressure):按需拉取,而非硬推
背压是 Reactor 区别于 CompletableFuture 和普通回调的本质特征。传统异步模型中,生产者可以无限制地推送数据,容易压垮消费者;而 Reactor 通过 request(n) 实现了精确的流量控制。
常见背压策略:
| 策略 | 行为 |
|---|---|
onBackpressureBuffer |
缓存暂时无法处理的数据,可设容量上限 |
onBackpressureDrop |
丢弃下游来不及处理的数据 |
onBackpressureLatest |
只保留最新数据,丢弃旧数据 |
onBackpressureError |
超出能力时直接报错 |
limitRate |
内部自动分批请求,采用预取策略平衡吞吐与内存 |
在 Spring WebFlux 中,当 HTTP 客户端消费响应流过慢时,底层的 Netty 连接会自动暂停读取,避免内存溢出。
五、算子链:每个算子都是一层 Publisher
操作符不会立即执行,而是生成新的 Publisher,将上下游串成一条链。数据流动时,从数据源一路向下经过每个算子的处理。
map:同步转换,通过FluxMap逐个拦截onNext,转换后继续向下游推送。flatMap:异步扁平转换,通过FluxFlatMap将内部产生的新 Publisher 订阅并合并到主数据流中,底层维护并发队列保证数据有序或无序推送,是异步编排的核心算子。filter:根据条件决定是否放行元素。retryWhen:捕获onError信号,通过新的订阅实例重新发起数据流订阅,支持自定义重试次数、间隔、异常匹配规则。onErrorResume:异常兜底降级,拦截异常后切换备用数据流,保证链路不中断。
Operator Fusion(操作符融合):Reactor 还会对相邻操作符进行融合优化,减少中间环节的数据拷贝和方法调用,降低层层包装带来的性能开销。
六、线程调度:Scheduler 体系
Reactor 不直接操作线程,而是通过 Scheduler 管理执行上下文。
核心调度器
| 调度器 | 适用场景 | 说明 |
|---|---|---|
Schedulers.parallel() |
CPU 密集型 | 固定大小线程池,默认线程数 = CPU 核数 |
Schedulers.boundedElastic() |
阻塞 I/O | 有界弹性线程池,适合 JDBC、同步 HTTP 调用 |
Schedulers.single() |
串行执行 | 单线程调度 |
Schedulers.immediate() |
当前线程 | 不切换线程 |
subscribeOn vs publishOn
这是最容易混淆的两个操作符:
| 操作符 | 作用范围 | 类比 |
|---|---|---|
subscribeOn(S) |
影响整个流的起始执行线程,无论放在链的哪个位置 | 决定产品在哪个工厂开始生产 |
publishOn(S) |
只影响它下游的操作符 | 从这里开始换哪家快递公司派送 |
Flux.fromIterable(ids)
.publishOn(Schedulers.boundedElastic()) // 从这里开始切换到弹性线程池
.map(id -> blockingQuery(id)) // 阻塞调用安全执行
.publishOn(Schedulers.parallel()) // 再切换到并行线程池
.map(result -> cpuIntensiveProcess(result)) // CPU 密集型计算
.subscribeOn(Schedulers.single()) // 流的起点在单线程上
.subscribe();
⚠️ 常见陷阱:把阻塞调用(如 JDBC)放到
parallel()线程池上会导致线程被吃光,必须使用boundedElastic()。
七、完整执行链路图
构建阶段(惰性) 订阅阶段(反向建立连接) 执行阶段(正向推送数据)
Flux.just(1,2,3) Subscriber 调用 subscribe() 数据源发出 onNext(1)
.map(x -> x*2) ↓ ↓
.filter(x -> x>2) 下游向上游反向订阅 map 转换: onNext(2)
.subscribe() ↓ ↓
数据源创建 Subscription filter 过滤: 2≤2, 丢弃
通过 onSubscribe 传递给下游 ↓
数据源发出 onNext(2)
↓
map 转换: onNext(4)
↓
filter 放行: 4>2 ✓
↓
Subscriber.onNext(4)
↓
... 直至 onComplete
八、总结
Flux 是多元素异步流,Mono 是单元素异步流;它们的底层都基于 Reactive Streams 的发布-订阅模型,通过以下五大机制协同工作:
- 惰性订阅:不
subscribe()就不执行,连数据源都不会启动。 - 算子链:每个操作符返回新 Publisher,形成责任链,支持 Operator Fusion 优化。
- Subscription:连接 Publisher 和 Subscriber 的契约,提供
request(n)和cancel()。 - 背压:下游通过
request(n)按需拉取,从根本上解决流量过载问题。 - Scheduler:通过
subscribeOn和publishOn精细控制线程执行上下文。
这套设计让 Reactor 能够用极少数线程高效处理海量并发连接,将系统瓶颈从线程数量转移到 CPU 核心处理能力上,这也是 Spring WebFlux 和 Spring Cloud Gateway 高吞吐能力的底层根源。
原理看完了,要不要我帮你整理一份 Flux/Mono 实战避坑指南?比如线程池误用、背压配置、异常处理这些高频踩坑点。

浙公网安备 33010602011771号