类型映射
| RxJava 3 | Reactor | 元素数 | 背压 |
|---|---|---|---|
Observable<T> | Flux<T> | 0..N | RxJava 无;Reactor 有 |
Flowable<T> | Flux<T> | 0..N | 两者都有 |
Single<T> | Mono<T> | 恰好 1 | 有 |
Maybe<T> | Mono<T> | 0 或 1 | 有 |
Completable | Mono<Void> | 0 | 有 |
Observable 和 Flux 看起来都是「0..N 个元素」,但这是迁移中最容易出事的一对:Observable 没有请求协议,Flux 有。下面的陷阱 1 会展开。
操作符映射
| RxJava | Reactor | 差异 |
|---|---|---|
map / filter | map / filter | 无 |
flatMap | flatMap | 默认并发数不同,见陷阱 5 |
concatMap | concatMap | 无 |
switchMap | switchMap | 无 |
zip / merge | zip / merge | 无 |
distinct | distinct | 无 |
delay | delayElements | 两者都逐元素延迟;Reactor 另有 delaySubscription |
doOnEach | doOnEach | Reactor 也存在,参数是 Signal<T> |
onErrorResumeNext | onErrorResume | 名称不同 |
onErrorReturn(Function) | 无直接等价 | 用 onErrorResume 计算兜底值 |
retryWhen | retryWhen(Retry) | Reactor 只接受 Retry |
sample(t) | sample(t) | 无 |
throttleFirst(t) | 无直接等价 | 不是 sample;需用 window 自己实现 |
throttleLast(t) | sample(t) | 同一个语义 |
debounce(t) | 无 debounce | 可用 sampleTimeout 表达 |
observeOn | publishOn | 名称不同 |
subscribeOn | subscribeOn | 无 |
有两个常见错误值得单独指出:
throttleFirst不能映射成sample。sample每个窗口取最后一个,与throttleLast相同;throttleFirst取的是窗口内第一个,Reactor 没有现成操作符。- Reactor 的
doOnEach一直存在,不需要拆成doOnNext+doOnError+doOnComplete。
调度器映射
| RxJava | Reactor | 说明 |
|---|---|---|
Schedulers.computation() | Schedulers.parallel() | CPU 密集,固定线程池 |
Schedulers.io() | Schedulers.boundedElastic() | 阻塞 I/O;Reactor 有队列上限 |
Schedulers.single() | Schedulers.single() | 单线程 |
Schedulers.newThread() | 无直接等价 | 用 fromExecutor 或 newBoundedElastic |
Schedulers.trampoline() | Schedulers.immediate() | 近似但不相同 |
trampoline 与 immediate 的差别在于:trampoline 把任务排入当前线程的队列,按顺序排空,能避免递归;immediate 直接在调用线程执行,递归仍然存在。如果原代码依赖 trampoline 的「排队」语义,迁移到 immediate 会改变调用顺序。
陷阱 1:Observable 没有背压
RxJava 的 Observable 不传递请求量。observeOn 用一个可增长的队列把元素从源线程搬到目标线程;下游慢,队列就涨。
实测:Observable.range(1, 1000) 配 observeOn(computation, delayError=false, bufferSize=1),下游每个元素睡眠 10ms:
got=228
3 秒内没有任何异常,队列持续吸收元素。bufferSize=1 只是初始容量,不构成硬上限。这意味着长时间运行的 Observable 加慢消费者,风险是内存增长,而不是一个可捕获的 MissingBackpressureException。
Reactor 的 publishOn 不同:它向上游请求有界预取量,下游消费多少,上游才被允许生产多少。慢消费者会让上游变慢,而不是让队列无限增长。如果源本身无法遵守请求(例如回调式数据源),Reactor 要求你显式选择溢出策略,例如 onBackpressureBuffer、onBackpressureDrop 或 onBackpressureError,失败会以 OverflowException 暴露。
迁移时先问:这个 Observable 的上游是否真的能降速?不能的话,换成 Flux 之后要么补一个溢出策略,要么保持 Observable 的语义(用 Flux.create(..., OverflowStrategy.DROP) 之类显式表达)。
陷阱 2:onErrorReturn 没有 Function 重载
RxJava 允许根据异常计算兜底值:
observable.onErrorReturn(throwable -> "fallback-" + throwable.getClass().getSimpleName());
Reactor 的 onErrorReturn 只有三种重载:
flux.onErrorReturn("fallback"); // 固定值
flux.onErrorReturn(IOException.class, "fallback"); // 按类型
flux.onErrorReturn(t -> t instanceof IOException, "fallback"); // 按谓词
注意第三种是「谓词 + 固定值」,不是「异常 → 值」。要计算兜底值,用 onErrorResume:
flux.onErrorResume(t -> Mono.just("fallback-" + t.getClass().getSimpleName()));
陷阱 3:Subject 的并发契约不同
RxJava 的裸 Subject 不会串行化 onNext。两个线程同时发射时,同一个观察者的 onNext 可能并发执行;实测最大并发数为 2。必须用 toSerialized() 包一层:
Subject<Integer> safe = PublishSubject.<Integer>create().toSerialized();
Reactor 的 Sinks.Many 默认检测并发发射,返回 FAIL_NON_SERIALIZED,并且 emitNext 在遇到该结果时抛 EmissionException。它不会替调用方排队,也不会静默并发进入下游:
Sinks.EmitResult result = sink.tryEmitNext(v);
if (result == Sinks.EmitResult.FAIL_NON_SERIALIZED) {
// 说明有并发发射,需要重新设计串行化
}
迁移时不要假设「Subject 能做的事 Sinks 也能直接做」:Sinks 把并发问题变成了显式失败,调用方必须处理。
陷阱 4:Disposable 是两种类型
| 库 | 类型 | 包 |
|---|---|---|
| RxJava | Disposable | io.reactivex.rxjava3.disposables |
| Reactor | Disposable | reactor.core |
方法名都是 dispose(),但类型不兼容。跨库抽象时不能共用一个字段,需要各自的适配层。RxJava 还有 CompositeDisposable;Reactor 没有同名工具,通常用 Disposables.composite() 或自己维护集合。
陷阱 5:flatMap 的默认并发数相差极大
RxJava 的 flatMap(mapper) 默认 maxConcurrency = Integer.MAX_VALUE(字节码里的常量 2147483647),也就是不限制同时订阅的内部流数量。
Reactor 的 flatMap(mapper) 默认 concurrency = Queues.SMALL_BUFFER_SIZE,即 256;内部预取量为 XS_BUFFER_SIZE(默认 32)。
对一个会产生大量内部流的源,这个差异直接决定并发压力:
// RxJava:可能同时订阅几千个内部流
source.flatMap(this::fetch);
// Reactor:最多 256 个
source.flatMap(this::fetch);
// 显式对齐
source.flatMap(this::fetch, 1024);
迁移后如果发现吞吐下降,先检查这里,而不是先怀疑调度器。
陷阱 6:Completable 没有元素,但完成信号要对齐
// RxJava
Completable.fromAction(() -> doWork()).subscribe();
// Reactor
Mono.fromRunnable(() -> doWork()).subscribe();
Mono.fromRunnable 推断为 Mono<T>,赋给 Mono<Void> 时由目标类型决定;也可以写 Mono.<Void>fromRunnable(...)。两者都在完成时发 onComplete,错误时发 onError。
如果原代码用 Completable.andThen(...) 串联,Reactor 对应 Mono.then(...):
// RxJava
Completable.fromAction(this::prepare).andThen(load());
// Reactor
Mono.fromRunnable(this::prepare).then(load());
迁移检查清单
- 把
Observable逐项确认:上游能否降速?不能就补溢出策略。 -
Single/Maybe→Mono,Completable→Mono<Void>。 -
onErrorReturn(Function)→onErrorResume。 -
retryWhen(Function)→Retry.withThrowable(...)或Retry.backoff(...)。 -
observeOn→publishOn,doOnDispose→doOnCancel。 - Subject 的
toSerialized()是否还需要?Sinks 侧是否处理了FAIL_NON_SERIALIZED? -
flatMap是否需要显式concurrency? - 调度器:
io→boundedElastic,computation→parallel,trampoline需要复核语义。 - 用
StepVerifier重写TestObserver断言。
性能比较不要照搬网上的数字
RxJava 与 Reactor 的吞吐差异高度依赖操作符融合、背压模式、调度器和 JVM 版本。脱离 JMH 的微基准很容易得出相反结论;不同机器、不同 JDK 上的绝对值也没有可比性。
如果确实需要比较,至少做到:
- 用 JMH,设置预热与足够的迭代;
- 固定 JDK、GC、CPU 亲和性和数据规模;
- 分别测「无背压的 Observable」与「有背压的 Flux」,不要混为一谈;
- 报告分位数与错误率,而不只是平均吞吐。
没有这四条,任何「Reactor 快 20%」的结论都不值得写进选型文档。
何时保留 RxJava
| 条件 | 建议 |
|---|---|
| Android 项目,团队与依赖都围绕 RxJava | 保留,迁移收益通常低于风险 |
大量 Observable 依赖无背压语义 | 保留,或先显式建模溢出策略 |
需要 AsyncSubject、UnicastSubject 等现成类型 | 保留,Reactor 只有近似替代 |
| 团队没有 Reactor 经验 | 先培训,不要为了「新」而迁移 |
何时迁移到 Reactor
| 条件 | 建议 |
|---|---|
| Spring WebFlux / Spring Data Reactive | 迁移,Reactor 是原生类型 |
| 需要端到端背压 | 迁移,Flux 的请求协议贯穿全链 |
| 新服务端项目 | 优先 Reactor |
需要与 JDK Flow 互操作 | 迁移,Reactor 提供适配 |
小结
| 类别 | 关键结论 |
|---|---|
| 类型 | Observable→Flux 不是无损替换,背压语义变了 |
| 错误 | Reactor 没有 onErrorReturn(Function),用 onErrorResume |
| 并发 | Subject 要 toSerialized();Sinks 用 FAIL_NON_SERIALIZED 报错 |
| flatMap | RxJava 默认无上限,Reactor 默认 256 |
| 调度器 | trampoline 与 immediate 只是近似 |
| 性能 | 没有 JMH 的数字不要当作选型依据 |
阅读导航
上一篇:10.2 ConnectableObservable 与 connect
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。