bigywater.tech
Spring · 55

Reactor 的惰性:没有订阅就什么都不发生

为什么我调用了 save() 数据库里却没有数据?Mono.just 和 Mono.fromSupplier 有什么不同?订阅两次会执行两次吗?cache() 和 share() 区别?错误去哪里了?这篇用 Reactor 3.8.7 逐个打印执行顺序。

0 行
构造 Mono.fromSupplier(...).map(...).doOnNext(...) 之后、订阅之前,链上打印的行数
3 → 1
同一个冷源订阅 3 次的执行次数,加上 cache() 之后
0
map 里调用返回 Mono 的 audit(x) 并丢掉返回值时,audit 实际执行的次数

组装与订阅

实测 · 本机执行输出
构造 Mono.fromSupplier(...).map(...).doOnNext(...)
构造完成,到这里为止上面没有任何输出;现在 subscribe
    (supplier 被执行)
    (map 被执行)
    (doOnNext 被执行:v!)
    (subscriber 收到 v!)
对照 Mono.just(expensive("A")):参数在构造时就算了
    (expensive(A) 被执行了)
Mono.just(...) 构造完成,尚未订阅
对照 Mono.fromSupplier(() -> expensive("B")):
构造完成,尚未订阅
    (expensive(B) 被执行了)
对照 Mono.defer(() -> Mono.just(expensive("C"))):
构造完成,尚未订阅
    (expensive(C) 被执行了)

链上没人订阅就什么都不发生;Mono.just 的参数是 Java 先算的。

冷源

实测 · 本机执行输出
同一个 Mono 订阅 3 次:
    (源被执行,第 1 次)
    订阅者收到 1
    (源被执行,第 2 次)
    订阅者收到 2
    (源被执行,第 3 次)
    订阅者收到 3
cache() 之后订阅 3 次:
    (源被执行,第 1 次)
    订阅者收到 1
    订阅者收到 1
    订阅者收到 1
Flux 同一个源被两个订阅者各订阅一次:
    (Flux 源被执行,第 1 次)
    订阅者 1 收到 10
    订阅者 1 收到 11
    (Flux 源被执行,第 2 次)
    订阅者 2 收到 20
    订阅者 2 收到 21
share() + 两个订阅者几乎同时订阅:
    (Flux 源被执行,第 1 次)
    订阅者 1 收到 10
    订阅者 2 收到 10
    订阅者 1 收到 11
    订阅者 2 收到 11

每次订阅重新执行;cache() 与 share() 才共用结果。

丢掉返回值

实测 · 本机执行输出
调用 save("x1") 但不订阅也不返回:
DB = []
调用 save("x2").subscribe():
    (save(x2) 真正写入)
DB = [x2]
在 map 里调用返回 Mono 的 audit(x),不订阅:
    结果 p
改成 flatMap(x -> audit(x).thenReturn(x)):
    (audit(p) 真正执行)
    结果 p
doOnNext(x -> audit(x))(同样丢了返回值):
    结果 p
doOnNext(x -> audit(x).subscribe())(手动订阅,线程/错误都脱离了主链):
    (audit(p) 真正执行)
    结果 p

返回 Mono 的方法必须接到链上,否则没有副作用。

热源

实测 · 本机执行输出
multicast 先发了 1、2,然后才订阅,再发 3:
    订阅者收到 1
    订阅者收到 2
    订阅者收到 3
directBestEffort(没有订阅者时发送)先发 1、2,再订阅,再发 3:
    订阅者收到 3
replay().all() 先发 1、2,再订阅,再发 3:
    订阅者收到 1
    订阅者收到 2
    订阅者收到 3
Flux.interval 是冷的:订阅者 B 晚 150 ms 订阅,也从 0 开始:
    A 收到 0
    A 收到 1
    B 收到 0
    A 收到 2
    B 收到 1
    B 收到 2

订阅时机决定收到什么;不同 Sink 的缓存策略不同。

CompletableFuture

实测 · 本机执行输出
构造 CompletableFuture.supplyAsync(...)(还没有任何订阅):
    (future 的任务在 ForkJoinPool.commonPool-worker-1 上被执行)
Mono.fromFuture(cf) 构造完成但还没订阅 —— 上面的任务其实早已执行
Mono.fromFuture(Supplier)(惰性):
构造完成,尚未订阅
现在订阅:
    (惰性的 future 任务被执行)
订阅 2 次 Mono.fromFuture(cf):
    收到 cf
    收到 cf

Future 本身是 eager 的,包成 Mono 也不会变惰性。

订阅方式

实测 · 本机执行输出
subscribe() 不会等待:
    subscribe() 返回,用时 1 ms
    (300 ms 的 delay 完成)
block() 会等待并抛出异常:
    block() 返回,用时 305 ms
    block() 抛出 IllegalStateException: boom
没有 error 回调的 subscribe():
    subscribe() 正常返回
有 error 回调的 subscribe(onNext, onError):
    onError 收到 boom3
-- 程序打在标准错误 / 标准输出上的非 RES 行(共 4 行),前 3 行:
  [ERROR] (main) Operator called default onErrorDropped - reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalStateException: boom2
  reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalStateException: boom2
  Caused by: java.lang.IllegalStateException: boom2

subscribe() 不等待,block() 等待,没有 error 回调的错误只进日志。

分步演示

惰性
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 reactor-core 3.8.7(Boot 4.0.8 BOM 管理的版本)的单个 Java 程序,不依赖 Spring 容器;每个现象由程序自己打印执行顺序;Flux.interval、share 的时间相关结果只做了一次运行
    • 没有验证:Mono/Flux 在 WebFlux 控制器里由框架订阅的时机(见第 58 篇);Hooks.onOperatorDebug 与 checkpoint 的调试信息;publish().refCount() 与 replay(n) 的行为;Schedulers 的线程切换(见第 57 篇);Reactor 的 Context(见第 62 篇);在 Reactor 的非阻塞线程上调用 block() 的后果
    京ICP备2026067502号-1京公网安备11010502063487号