为什么需要 Sinks
第 2 章手写的 Publisher 只在 subscribe() 时产生数据。但很多数据来自订阅之外的异步事件:
- WebSocket 收到消息;
- 数据库变更监听回调;
- 用户点击、消息队列投递。
Flux.create 可以桥接这类回调,但它把发射能力藏在 create 的 lambda 里,外部拿不到一个稳定的句柄。Sinks 把「发射端」显式暴露出来:
Sinks.Many<String> sink = Sinks.many().multicast().onBackpressureBuffer();
// 消费端
sink.asFlux().subscribe(System.out::println);
// 生产端,可以在任意回调里调用
sink.tryEmitNext("hello");
sink.tryEmitNext("world");
sink.tryEmitComplete();
实测输出:
hello
world
Sinks.Many 的三种模式
// 单播:只允许一个订阅者;订阅前的元素会保留,订阅后回放
Sinks.Many<String> unicast = Sinks.many().unicast().onBackpressureBuffer();
// 多播:多个订阅者共享;订阅前的元素作为 warm-up 缓冲
Sinks.Many<String> multicast = Sinks.many().multicast().onBackpressureBuffer();
// 重放:为后订阅者保留历史
Sinks.Many<String> replay = Sinks.many().replay().limit(10);
三者的共同点是「发射端独立于订阅者」,区别在于多订阅者和历史保留:
| 模式 | 订阅者数 | 订阅前的元素 | 后订阅者 |
|---|---|---|---|
unicast | 只允许 1 个 | 保留,订阅后回放 | 不适用 |
multicast().onBackpressureBuffer() | N 个 | 最多缓冲 bufferSize 个 | 只收到订阅后的新元素 |
multicast().directBestEffort() | N 个 | 不缓冲,零订阅者时发射失败 | 只收到订阅后的新元素 |
replay().limit(n) | N 个 | 保留最近 n 个 | 收到最近 n 个历史 |
零订阅者时是否接受发射,取决于实现。实测:
multicast buffer zero-subscriber: OK
best-effort zero-subscriber: FAIL_ZERO_SUBSCRIBER
replay zero-subscriber: OK
unicast zero-subscriber: OK
带缓冲的多播、单播和 replay 都允许在没人订阅时先发射(缓冲或历史);directBestEffort 和 directAllOrNothing 会返回 FAIL_ZERO_SUBSCRIBER。
发射 API:tryEmitNext 与 emitNext
Sinks.Many 有两组发射方法:
| 方法 | 返回 | 失败时 |
|---|---|---|
tryEmitNext(v) | EmitResult | 只返回结果,sink 仍可用 |
emitNext(v, handler) | void | 按 handler 决定是否重试;不重试时可能终止 sink |
tryEmitNext 是「尝试一次、把结果交给你」:
Sinks.EmitResult result = sink.tryEmitNext("data");
if (result.isFailure()) {
log.warn("emit failed: {}", result);
}
emitNext 需要显式传入失败处理器。FAIL_FAST 表示不重试:
sink.emitNext("data", Sinks.EmitFailureHandler.FAIL_FAST);
这里有个容易忽略的行为:emitNext 遇到 FAIL_OVERFLOW 时,不只是丢弃当前元素,还会把整条 sink 以 OverflowException 终止。源码里的处理是 emitError(failWithOverflow("Backpressure overflow during Sinks.Many#emitNext"))。
实测(onBackpressureBuffer(1)、订阅者不请求、连续 emitNext):
subscriber error: OverflowException: Backpressure overflow during Sinks.Many#emitNext
later try=FAIL_TERMINATED
对比 tryEmitNext 在同样场景下的结果:
try 0=OK
try 1=FAIL_OVERFLOW
try 2=FAIL_OVERFLOW
try 3=FAIL_OVERFLOW
try 4=FAIL_OVERFLOW
订阅者没有收到错误,sink 也没有终止;后续发射仍会返回 FAIL_OVERFLOW。如果要「溢出时丢当前元素、但继续服务」,用 tryEmitNext 并自己处理结果;如果要「溢出即失败」,用 emitNext。
EmitResult
| 结果 | 含义 | 典型处理 |
|---|---|---|
OK | 已接受 | 继续 |
FAIL_OVERFLOW | 缓冲已满,或没有订阅者能接收 | 丢弃、降级或告警 |
FAIL_ZERO_SUBSCRIBER | 没有订阅者,且该模式不接受预热缓冲 | 忽略或等待订阅者 |
FAIL_CANCELLED | 下游已取消 | 停止发射 |
FAIL_TERMINATED | sink 已完成或已出错 | 停止发射 |
FAIL_NON_SERIALIZED | 检测到并发发射 | 序列化后重试,否则会丢数据 |
FAIL_NON_SERIALIZED 是理解 Sinks 并发模型的关键,下一节展开。
线程安全:默认 Sinks 不序列化并发发射
一个流传很广的说法是「Sinks 是线程安全的,可以从任意线程发射」。这个说法不准确。默认 Sinks 会检测并发发射并返回 FAIL_NON_SERIALIZED,但它不会替你把并发发射排成序。
实测:两个线程各向同一个 multicast().onBackpressureBuffer() 发射 500 个元素,并发开始:
results={FAIL_NON_SERIALIZED=772, OK=228}
received=228
1000 次尝试只有 228 次成功,772 次因为并发冲突被拒绝;下游只收到 228 个元素。如果代码不检查返回值,就会静默丢数据。
正确做法有两种:
- 自己序列化:所有发射都从一个线程或一把锁里发出;
- 用
Sinks.unsafe().many():它跳过并发检测,调用方必须保证串行。它不会让并发变安全,只是把检测开销去掉;并发调用会导致未定义行为。
// 安全:所有发射都在同一把锁内
synchronized (lock) {
sink.tryEmitNext(value);
}
// 或者:明确由单线程发射
emitterThread.submit(() -> sink.tryEmitNext(value));
emitNext 遇到 FAIL_NON_SERIALIZED 不会重试(FAIL_FAST),而是抛出 Sinks.EmissionException:
Spec. Rule 1.3 - onSubscribe, onNext, onError and onComplete signaled to a Subscriber MUST be signaled serially.
这条异常信息直接指向 Reactive Streams 的串行信号规则。
发射端规则
除了串行,发射端还要遵守几件事:
- 终止只发一次。
tryEmitComplete()或tryEmitError()成功后,后续发射返回FAIL_TERMINATED。 - 不要在
onNext里递归发射。回调里再次tryEmitNext可能触发并发/重入检测;需要级联时用concatMap或异步边界。 - 检查返回值,至少监控失败。
FAIL_OVERFLOW、FAIL_NON_SERIALIZED都不是「可忽略」的成功。 - 完成时释放外部资源。注册
onCancel/onDispose回调,关闭 WebSocket、取消监听器。
与 Processor 的对比
| 特性 | Sinks(3.4+) | Processor(旧 API) |
|---|---|---|
| 发射结果 | EmitResult 枚举 | 多数方法无返回值 |
| 失败处理 | 显式 handler | 需自行判断 |
| 并发发射 | 检测并返回 FAIL_NON_SERIALIZED | 行为依实现而异 |
| 推荐程度 | 新代码首选 | 已弃用,逐步移除 |
Sinks 并没有把并发问题变没,只是把它从「未定义行为」变成「可检测的失败结果」。这已经比旧 Processor 好,但仍然要求调用方处理结果。
小结
| 关注点 | 结论 |
|---|---|
| 三种 Many | unicast / multicast / replay,按订阅者数与历史需求选择 |
| 零订阅者 | 带缓冲的多播、单播、replay 接受;direct 模式返回 FAIL_ZERO_SUBSCRIBER |
| 发射 | tryEmitNext 返回结果;emitNext 溢出时会终止 sink |
| 并发 | 默认不序列化,并发发射返回 FAIL_NON_SERIALIZED |
| 序列化 | 单线程、加锁,或用 Sinks.unsafe() 并自行保证串行 |
阅读导航
上一篇:8.3 refCount、autoConnect 与 cache
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。