Project Reactor 是Java响应式编程库 ,提供Mono 和Flux 核心类型,支持非阻塞、背压及异步数据流处理 。它是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 中,这套机制的核心抽象就是 Scheduler。
默认情况下,Flux 和 Mono 的操作符链在当前线程上执行。如果你需要将部分工作切换到特定线程池(如 I/O 线程池、CPU 计算线程池),就需要通过 subscribeOn() 和 publishOn() 配合 Scheduler 来实现。
本文将系统梳理 Reactor 的调度器体系:从内置 Scheduler 的语义与线程模型,到 subscribeOn 与 publishOn 的精确行为差异,再到工程实践中的调度策略与常见陷阱。
一、Scheduler 接口:调度器的抽象
1.1 核心定义
java
public interface Scheduler extends Disposable {
Disposable schedule(Runnable task);
Disposable schedule(Runnable task, long delay, TimeUnit unit);
Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit);
Worker createWorker();
void start();
void dispose();
boolean isDisposed();
long now(TimeUnit unit);
}
Scheduler 本质上是一个任务提交与执行的抽象层------它屏蔽了底层线程池的具体实现(ExecutorService、ForkJoinPool、单线程、ScheduledExecutorService 等),向 Reactor 操作符提供统一的 schedule(Runnable) 接口。
1.2 Worker:调度器的工作单元
java
public interface Worker extends Disposable {
Disposable schedule(Runnable task);
Disposable schedule(Runnable task, long delay, TimeUnit unit);
Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit);
void dispose();
}
Worker 代表从 Scheduler 中获取的一个执行上下文。同一个 Worker 上的任务保证顺序执行(FIFO),不同 Worker 之间可能并行。
二、内置 Scheduler 全景
Reactor 通过 Schedulers 工具类提供了多种开箱即用的调度器:
2.1 速查表
| Scheduler | 线程模型 | 线程数 | 适用场景 | 是否共享 |
|---|---|---|---|---|
| Schedulers.immediate() | 当前线程 | 0(不创建新线程) | 测试、无切换需求 | --- |
| Schedulers.single() | 共享单线程 | 1 | 低延迟串行任务、定时器 | ✅ 全局共享 |
| Schedulers.parallel() | 固定线程池 | CPU 核数 | CPU 密集型计算 | ✅ 全局共享 |
| Schedulers.boundedElastic() | 弹性有界线程池 | 10 × CPU 核数(上限) | I/O 阻塞操作 | ✅ 全局共享 |
| Schedulers.fromExecutor(executor) | 自定义 Executor | 取决于 Executor | 桥接已有线程池 | ❌ 每次新建 |
| Schedulers.fromExecutorService(es) | 自定义 ExecutorService 取决于 ES | 精细控制线程池 | ❌ 每次新建 | |
| --- |
2.2 Schedulers.immediate()
java
Scheduler immediate = Schedulers.immediate();
- 不创建新线程,任务在当前调用线程上立即执行
- 相当于"无调度"
- 主要用于测试或明确不需要线程切换的场景
2.3 Schedulers.single()
java
Scheduler single = Schedulers.single();
- 所有任务在同一个共享线程上顺序执行
- 适合:低延迟定时任务、顺序事件处理、需要串行保证的场景
- ⚠️ 如果某个任务阻塞,后续所有任务都会被阻塞
- 变体:Schedulers.newSingle("name") 创建独立的新单线程调度器
java
// 使用场景:确保事件按序处理
Flux<Event> events = eventSource.getEvents()
.publishOn(Schedulers.single())
.subscribe(event -> processSequentially(event));
2.4 Schedulers.parallel()
java
Scheduler parallel = Schedulers.parallel();
- 固定大小线程池,线程数 = Runtime.getRuntime().availableProcessors()(CPU 核数)
- 适合:CPU 密集型任务(数据变换、计算、编解码)
- ⚠️ 绝对不要在此线程池上执行阻塞操作------线程数有限,阻塞会导致线程饥饿
- 变体:Schedulers.newParallel("name", n) 创建自定义大小的并行调度器
java
// CPU 密集型计算
Flux.range(1, 1000)
.parallel()
.runOn(Schedulers.parallel())
.map(i -> expensiveComputation(i))
.sequential()
.subscribe();
2.5 Schedulers.boundedElastic()(重点)
java
Scheduler boundedElastic = Schedulers.boundedElastic();
这是 Reactor 3.3+ 引入的调度器,替代了已废弃的 Schedulers.elastic()。 核心特性:
| 参数 | 默认值 | 说明 |
|---|---|---|
| 最大线程数 | 10 × CPU 核数 | 可配置上限 |
| 最大排队任务数 | 100,000 / 线程 | 超出则拒绝 |
| 空闲线程存活时间 | 60 秒 | 超时后回收 |
| 线程创建策略 | 按需创建 | 负载增加时扩展 |
设计意图:
为阻塞 I/O 操作提供一个有上限的弹性线程池,避免无限创建线程导致 OOM。
java
// ✅ 正确:将阻塞 JDBC 调用切换到 boundedElastic
Mono<User> user = Mono.fromCallable(() -> {
// 阻塞操作
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?", new UserMapper(), id
);
}).subscribeOn(Schedulers.boundedElastic());
与已废弃的 elastic() 对比:
| 维度 | elastic()(已废弃) | boundedElastic() |
|---|---|---|
| 线程上限 | ❌ 无限制 | ✅ 10 × CPU 核数 |
| 任务队列 | 无界 | 有界(100K/线程) |
| OOM 风险 | 高 | 低 |
| 推荐状态 | ❌ 废弃 | ✅ 推荐 |
2.6 自定义 Scheduler
java
// 从已有 Executor 创建
ExecutorService myPool = Executors.newFixedThreadPool(4);
Scheduler customScheduler = Schedulers.fromExecutorService(myPool);
// 从已有 Executor 创建(无法 dispose)
Executor executor = ForkJoinPool.commonPool();
Scheduler forkJoinScheduler = Schedulers.fromExecutor(executor);
// 使用完毕后释放
customScheduler.dispose();
2.7 JDK 21+ 虚拟线程调度器
java
// Spring Framework 6.1+ / Reactor 3.6+ 支持
Scheduler virtualThreadScheduler = Schedulers.fromExecutorService(
Executors.newVirtualThreadPerTaskExecutor()
);
Mono<User> user = Mono.fromCallable(() -> blockingService.getUser(id))
.subscribeOn(virtualThreadScheduler);
三、subscribeOn vs publishOn:两大调度操作符
这是 Reactor 线程调度中最核心、也最易混淆的两个操作符。
3.1 subscribeOn(Scheduler)
影响整个订阅过程和数据源的发射线程。
java
Flux<Integer> flux = Flux.range(1, 5)
.map(i -> {
System.out.println("map: " + Thread.currentThread().getName());
return i * 2;
})
.subscribeOn(Schedulers.parallel()); // 整个链在 parallel 线程池执行
flux.subscribe(i -> System.out.println("subscribe: " + Thread.currentThread().getName()));
关键行为:
- 无论 subscribeOn 放在链的哪个位置,效果相同(因为它影响的是订阅信号,而订阅信号是从下游向上游传播的)
- 影响数据源的发射线程
- 影响其上方所有操作符的执行线程
- 如果链中有多个 subscribeOn,最靠近源头的(最上面的)生效
java
Flux.range(1, 5)
.map(i -> i) // ← 在 parallel 线程执行
.subscribeOn(Schedulers.parallel()) // 影响源头和上方
.map(i -> i) // ← 也在 parallel 线程执行(除非被 publishOn 改变)
.subscribeOn(Schedulers.boundedElastic()) // ❌ 无效!第一个 subscribeOn 已生效
.subscribe();
3.2 publishOn(Scheduler)
影响其下方操作符的执行线程(数据信号的切换点)。
java
Flux.range(1, 5)
.map(i -> {
System.out.println("Before publishOn: " + Thread.currentThread().getName());
return i;
})
.publishOn(Schedulers.parallel()) // 从这里开始切换线程
.map(i -> {
System.out.println("After publishOn: " + Thread.currentThread().getName());
return i * 2;
})
.subscribe(i -> System.out.println("Subscribe: " + Thread.currentThread().getName()));
关键行为:
- 位置敏感------只影响其下方的操作符
- 可以出现多次,每次切换都生效
- 影响的是数据信号(onNext/onError/onComplete) 的执行线程
- 不影响数据源的发射线程
3.3 核心对比
| 维度 | subscribeOn | publishOn |
|---|---|---|
| 影响范围 | 整个订阅过程(源头 + 上方) | 仅下方操作符 |
| 位置敏感性 | ❌ 不敏感(放哪都一样) | ✅ 敏感(只影响下方) |
| 多次调用 | 只有最靠近源头的生效 | 每次都生效 |
| 影响的信号 | 订阅信号 + 数据发射 | 仅数据信号 |
| 典型用途 | 切换数据源/阻塞操作线程 | 切换下游处理线程 |
3.4 组合使用的经典模式
java
Mono.fromCallable(() -> {
// ① 阻塞 I/O:在 boundedElastic 线程池执行
return blockingDatabase.query(id);
})
.subscribeOn(Schedulers.boundedElastic()) // ② 源头切到弹性线程池
.map(data -> {
// ③ 仍在 boundedElastic 线程
return transform(data);
})
.publishOn(Schedulers.parallel()) // ④ 从这里开始切到 parallel
.map(data -> {
// ⑤ CPU 密集计算:在 parallel 线程池执行
return expensiveCompute(data);
})
.subscribe(result -> {
// ⑥ 仍在 parallel 线程
System.out.println(result);
});
线程切换时序图:
文本
Thread: boundedElastic-1 Thread: parallel-1
───────────────────────── ─────────────────────
① blockingDatabase.query()
② subscribe 信号传播
③ transform(data)
④ publishOn 切换点
⑤ expensiveCompute(data)
⑥ subscribe(result)
3.5 多次 publishOn 切换
java
Flux.range(1, 3)
.map(i -> "A" + i) // Thread: main
.publishOn(Schedulers.parallel())
.map(s -> "B" + s) // Thread: parallel-1
.publishOn(Schedulers.boundedElastic())
.map(s -> "C" + s) // Thread: boundedElastic-1
.subscribe(s -> System.out.println(s)); // Thread: boundedElastic-1
四、调度器与操作符的交互
4.1 哪些操作符自带调度器?
某些操作符内部默认使用特定 Scheduler:
| 操作符 | 默认 Scheduler | 说明 |
|---|---|---|
| Flux.interval(Duration) | Schedulers.parallel() | 定时发射 |
| Mono.delay(Duration) | Schedulers.parallel() | 延迟发射 |
| Flux.timeout(Duration) | Schedulers.parallel() | 超时检测 |
| delayElements(Duration) | Schedulers.parallel() | 元素延迟 |
| delaySubscription(Duration) | Schedulers.parallel() | 订阅延迟 |
java
// interval 默认在 parallel 线程池
Flux.interval(Duration.ofSeconds(1))
.subscribe(i -> System.out.println(Thread.currentThread().getName()));
// 输出: parallel-1
// 可以覆盖默认调度器
Flux.interval(Duration.ofSeconds(1), Schedulers.single())
.subscribe(i -> System.out.println(Thread.currentThread().getName()));
// 输出: single-1
4.2 flatMap 中的并发与调度
java
// flatMap 默认并发度 256,内部元素可能在 parallel 线程执行
Flux.range(1, 100)
.flatMap(i -> Mono.fromCallable(() -> blockingCall(i))
.subscribeOn(Schedulers.boundedElastic()),
32) // 限制并发度为 32
.subscribe();
五、工程实战场景
5.1 WebFlux 中隔离阻塞操作
java
@Service
public class LegacyService {
// ❌ 错误:直接在 EventLoop 线程执行阻塞调用
public Mono<User> getUserBad(Long id) {
return Mono.just(jdbcTemplate.queryForObject(...)); // 阻塞 EventLoop!
}
// ✅ 正确:切换到 boundedElastic
public Mono<User> getUserGood(Long id) {
return Mono.fromCallable(() ->
jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new UserRowMapper(), id
)
).subscribeOn(Schedulers.boundedElastic());
}
}
5.2 CPU 密集型计算与 I/O 分离
java
public Mono<Report> generateReport(Long userId) {
return Mono.fromCallable(() -> fetchDataFromLegacyApi(userId)) // I/O
.subscribeOn(Schedulers.boundedElastic()) // I/O 线程
.map(rawData -> parseAndValidate(rawData)) // 仍在 I/O 线程
.publishOn(Schedulers.parallel()) // 切换到 CPU 线程
.map(data -> computeStatistics(data)) // CPU 密集计算
.map(stats -> formatReport(stats)); // 仍在 CPU 线程
}
5.3 并行处理 + 结果聚合
java
public Mono<Dashboard> loadDashboard(Long userId) {
return Mono.zip(
userService.getUser(userId)
.subscribeOn(Schedulers.boundedElastic()),
orderService.getRecentOrders(userId)
.subscribeOn(Schedulers.boundedElastic()),
analyticsService.computeMetrics(userId)
.subscribeOn(Schedulers.parallel())
).map(tuple -> new Dashboard(tuple.getT1(), tuple.getT2(), tuple.getT3()));
}
5.4 定时任务与调度
java
// 每秒轮询一次
Flux.interval(Duration.ofSeconds(1))
.flatMap(tick -> Mono.fromCallable(() -> pollExternalService())
.subscribeOn(Schedulers.boundedElastic()))
.subscribe(data -> processNewData(data));
// 延迟执行
Mono.delay(Duration.ofMinutes(5))
.then(cleanupTask())
.subscribe();
// 定期执行
Flux.interval(Duration.ofHours(1), Schedulers.parallel())
.flatMap(tick -> scheduledJob())
.subscribe();
5.5 测试中控制调度器
java
@Test
void testWithVirtualTime() {
// 使用 VirtualTimeScheduler 避免真实等待
StepVerifier.withVirtualTime(() ->
Mono.delay(Duration.ofHours(2)).then(Mono.just("done"))
)
.expectSubscription()
.expectNoEvent(Duration.ofHours(2))
.expectNext("done")
.verifyComplete();
}
@Test
void testWithImmediateScheduler() {
// 使用 immediate 确保在当前线程执行(便于断言)
Mono<String> result = Mono.fromCallable(() -> "hello")
.subscribeOn(Schedulers.immediate());
StepVerifier.create(result)
.expectNext("hello")
.verifyComplete();
}
六、Scheduler 的生命周期管理
6.1 共享 Scheduler 的复用
java
// 全局共享,不需要手动 dispose
Scheduler shared = Schedulers.parallel(); // 全局单例
Scheduler shared2 = Schedulers.boundedElastic(); // 全局单例
多次调用 Schedulers.parallel() 返回的是同一个实例。
6.2 自定义 Scheduler 的释放
java
// 自定义调度器需要手动管理生命周期
Scheduler custom = Schedulers.newParallel("my-pool", 4);
try {
Flux.range(1, 100)
.parallel()
.runOn(custom)
.map(i -> compute(i))
.sequential()
.blockLast();
} finally {
custom.dispose(); // ⚠️ 必须释放!否则线程泄漏
}
6.3 Spring 容器中的管理
java
@Configuration
public class SchedulerConfig {
@Bean(destroyMethod = "dispose")
public Scheduler ioScheduler() {
return Schedulers.newBoundedElastic(
16, // 最大线程数
1000, // 每线程最大排队数
"io-pool" // 线程名前缀
);
}
@Bean(destroyMethod = "dispose")
public Scheduler cpuScheduler() {
return Schedulers.newParallel("cpu-pool", 4);
}
}
七、深入理解:线程切换的底层机制
7.1 publishOn 的内部实现
publishOn 本质上是在操作符链中插入了一个异步队列:
文本
上游线程 → [Queue] → 下游线程
↑
publishOn 在这里
将 onNext 信号放入队列
下游 Worker 从队列中取出并处理
这意味着:
- publishOn 引入了一次线程上下文切换
- 存在微小的延迟(队列的入队/出队)
- 对于高频小数据,过多的 publishOn 会影响性能
7.2 subscribeOn 的传播机制
java
Flux.range(1, 5) // source
.map(...) // op1
.subscribeOn(Schedulers.parallel()) // 调度点
.filter(...) // op2
.subscribe(); // terminal
执行流程:
文本
1. subscribe() 在 main 线程被调用
2. 订阅信号向上传播:terminal → op2 → [subscribeOn 拦截]
3. subscribeOn 将后续订阅操作提交到 parallel 线程池
4. 在 parallel 线程中:继续向上订阅 → op1 → source
5. source 在 parallel 线程中开始发射数据
6. 数据沿链向下流动,默认仍在 parallel 线程
八、常见陷阱与最佳实践
8.1 ❌ 在 EventLoop/parallel 线程中执行阻塞操作
java
// ❌ 灾难性错误!阻塞了 EventLoop 线程
Flux.range(1, 10)
.map(i -> {
Thread.sleep(1000); // 阻塞 parallel 线程!
return i;
})
.subscribeOn(Schedulers.parallel())
.subscribe();
后果:parallel 线程池只有 CPU 核数个线程,一个阻塞就少一个可用线程,最终导致线程饥饿。
java
// ✅ 正确:阻塞操作必须在 boundedElastic
Flux.range(1, 10)
.flatMap(i -> Mono.fromCallable(() -> {
Thread.sleep(1000); // 阻塞在弹性线程池,安全
return i;
}).subscribeOn(Schedulers.boundedElastic()))
.subscribe();
8.2 ❌ 过多的 publishOn 切换
java
// ❌ 性能杀手:频繁线程切换
Flux.range(1, 1000000)
.publishOn(Schedulers.parallel())
.map(i -> i + 1)
.publishOn(Schedulers.boundedElastic())
.map(i -> i * 2)
.publishOn(Schedulers.parallel())
.map(i -> i - 1)
.subscribe();
java
// ✅ 正确:只在必要时切换
Flux.range(1, 1000000)
.publishOn(Schedulers.parallel())
.map(i -> i + 1)
.map(i -> i * 2)
.map(i -> i - 1)
.subscribe();
8.3 ❌ subscribeOn 位置误解
java
// 以下两种写法效果完全相同:
Flux.range(1, 5).map(...).subscribeOn(Schedulers.parallel()).subscribe();
Flux.range(1, 5).subscribeOn(Schedulers.parallel()).map(...).subscribe();
// subscribeOn 的位置不影响效果!
8.4 ❌ 忽略 boundedElastic 的容量限制
java
// ⚠️ boundedElastic 有上限:10 × CPU 核数
// 在 8 核机器上,最多 80 个线程
// 如果同时有 1000 个阻塞任务,大部分会排队等待
// 解决方案:限制并发度
Flux.fromIterable(hugeList)
.flatMap(item -> blockingCall(item)
.subscribeOn(Schedulers.boundedElastic()),
50) // 限制最多 50 个并发
.subscribe();
8.5 ✅ 最佳实践清单
| # | 实践 | 说明 |
|---|---|---|
| 1 | 阻塞 I/O → boundedElastic | 永远不要在 parallel/EventLoop 上阻塞 |
| 2 | CPU 密集 → parallel | 线程数 = CPU 核数,避免过度并行 |
| 3 | 串行保序 → single | 需要严格顺序执行时 |
| 4 | 最少化 publishOn | 每次切换都有上下文切换开销 |
| 5 | subscribeOn 只写一次 | 多次调用只有第一个生效 |
| 6 | 自定义 Scheduler 必须 dispose | 防止线程泄漏 |
| 7 | 用 flatMap 的 concurrency 参数限流 | 避免压垮 boundedElastic |
| 8 | 测试用 VirtualTimeScheduler | 避免真实等待 |
| 9 | 生产环境用共享 Scheduler | 避免频繁创建/销毁线程池 |
| 10 | JDK 21+ 考虑虚拟线程 | Executors.newVirtualThreadPerTaskExecutor() |
九、调度策略选择决策树
文本
你的任务是什么类型?
│
├── 阻塞 I/O(JDBC、文件、同步 HTTP)?
│ └── → Schedulers.boundedElastic()
│ + subscribeOn()
│ + 限制并发度(flatMap concurrency)
│
├── CPU 密集计算(编解码、数据变换)?
│ └── → Schedulers.parallel()
│ + publishOn() 或 parallel().runOn()
│
├── 需要严格串行执行?
│ └── → Schedulers.single()
│ + publishOn()
│
├── 定时/延迟任务?
│ └── → 默认 parallel(Flux.interval)
│ 或自定义 ScheduledExecutor
│
├── 桥接已有线程池?
│ └── → Schedulers.fromExecutorService(yourPool)
│
├── JDK 21+ 虚拟线程?
│ └── → Schedulers.fromExecutorService(
│ Executors.newVirtualThreadPerTaskExecutor())
│
└── 不需要切换线程?
└── → 不调用 subscribeOn/publishOn(默认当前线程)
十、监控与调优
10.1 线程命名
java
// 自定义线程名前缀,方便日志排查
Scheduler ioScheduler = Schedulers.newBoundedElastic(16, 1000, "my-io");
Scheduler cpuScheduler = Schedulers.newParallel("my-cpu", 4);
// 日志输出示例:
// [my-io-1] Executing database query...
// [my-cpu-3] Computing statistics...
10.2 线程池监控
java
// 通过 Micrometer 暴露 Scheduler 指标
Schedulers.onScheduleHook("metrics", runnable -> {
// 包装任务以记录指标
return () -> {
Timer.Sample sample = Timer.start(registry);
try {
runnable.run();
} finally {
sample.stop(registry.timer("scheduler.task"));
}
};
});
10.3 全局钩子
java
// 全局调度钩子:在所有任务调度时执行
Schedulers.onScheduleHook("logging", runnable -> () -> {
System.out.println("Task scheduled on: " + Thread.currentThread().getName());
runnable.run();
});
// 移除钩子
Schedulers.resetOnScheduleHook("logging");
十一、与 Spring WebFlux 的集成
11.1 Netty EventLoop 线程
Spring WebFlux 默认运行在 Netty EventLoop 线程上(类似 parallel,线程数 = CPU 核数)。
java
// 在 Controller 中,当前线程就是 Netty EventLoop
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
// ⚠️ 这里运行在 EventLoop 线程上
// 绝对不能执行阻塞操作!
return userRepository.findById(id); // R2DBC:非阻塞,安全
}
11.2 混合阻塞与非阻塞
java
@GetMapping("/report/{id}")
public Mono<Report> getReport(@PathVariable Long id) {
return reportService.generateAsync(id) // 非阻塞 R2DBC
.flatMap(data -> Mono.fromCallable(() ->
legacyPdfGenerator.createPdf(data) // 阻塞 PDF 生成
).subscribeOn(Schedulers.boundedElastic())) // 切到弹性线程池
.publishOn(Schedulers.parallel()) // 切回 CPU 线程
.map(pdf -> compressPdf(pdf)); // CPU 密集压缩
}
十二、总结
文本
┌──────────────────────────────────────────────────────────────────────┐
│ Reactor Scheduler 核心知识图谱 │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ Scheduler 类型: │
│ ┌─────────────────┬────────────────────────────────────────┐ │
│ │ immediate │ 当前线程,无切换 │ │
│ │ single │ 共享单线程,串行 │ │
│ │ parallel │ 固定线程池(CPU核数),CPU密集 │ │
│ │ boundedElastic │ 弹性有界线程池(10×CPU),I/O阻塞 │ │
│ │ fromExecutor │ 自定义线程池桥接 │ │
│ └─────────────────┴────────────────────────────────────────┘ │
│ │
│ 调度操作符: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ subscribeOn(Scheduler) │ │
│ │ • 影响源头 + 上方所有操作符 │ │
│ │ • 位置不敏感 │ │
│ │ • 只有最靠近源头的生效 │ │
│ │ • 用途:切换数据源/阻塞操作线程 │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ publishOn(Scheduler) │ │
│ │ • 只影响下方操作符 │ │
│ │ • 位置敏感 │ │
│ │ • 可多次调用 │ │
│ │ • 用途:切换下游处理线程 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 黄金法则: │
│ • 阻塞 → boundedElastic + subscribeOn │
│ • CPU密集 → parallel + publishOn │
│ • 不要在 EventLoop/parallel 上阻塞 │
│ • 最小化线程切换次数 │
│ • 自定义 Scheduler 必须 dispose │
│ │
└──────────────────────────────────────────────────────────────────────┘
Scheduler 是 Reactor 并发模型的基石。理解每种调度器的线程模型、掌握 subscribeOn 与 publishOn 的精确语义、遵循"阻塞隔离"原则,是构建高性能响应式应用的必备技能。
在微服务架构中,一个请求可能跨越数据库查询(boundedElastic)、数据变换(parallel)、消息推送(single)等多个调度域。正确的调度策略,就是让这些异构操作各得其所、互不干扰的编排艺术。