Buffer 解决的是「下游需求不足」,不是「上游太快」
onBackpressureBuffer 的机制很容易被误解。它在 onSubscribe 里会向上游请求 Long.MAX_VALUE,然后把元素放进自己的队列,等下游 request(n) 时再按需发出:
// FluxOnBackpressureBuffer 的订阅逻辑(简化)
actual.onSubscribe(this);
s.request(Long.MAX_VALUE); // 先向上游要满
这意味着两件事:
- 它不会让上游变慢。上游会以自己能跑的最快速度把元素全部推给它。
- 它只在下游确实没有请求时才会堆积。下游如果请求
Long.MAX_VALUE,队列会立刻被抽干。
但反过来,把 buffer 放在慢消费者前面,它就会真的堆积。delayElements(Duration) 内部是 delayUntil(d -> Mono.delay(...)),而 delayUntil 用的是不带预取的 concatMap:取一个元素,等它的延迟完成,再请求下一个。也就是说它同一时刻只向上游要 1 个元素,不是无界消费者。
用同步源可以把这一点确定下来:
Flux.range(1, 20)
.onBackpressureBuffer(2, v -> dropped.incrementAndGet(),
BufferOverflowStrategy.DROP_LATEST)
.delayElements(Duration.ofMillis(100))
.subscribe(v -> consumed.incrementAndGet());
range 在 subscribe() 返回前就把 20 个元素推给 buffer;delayElements 只取走 1 个(正在延迟中),buffer 容量是 2,其余 17 个被丢弃:
after subscribe: dropped=17 consumed=0
after 350ms: dropped=17 consumed=3
所以顺序决定了谁在积压:onBackpressureBuffer 放在慢消费者前面时,队列会持续增长;要观察溢出,这是最直接的写法。真正会主动从 buffer 里取走一批元素的是带预取的操作符(例如 publishOn 按预取窗口取数),而不是 delayElements。
重载与默认语义
| 重载 | 缓冲上限 | 溢出行为 | 队列实现 |
|---|---|---|---|
onBackpressureBuffer() | 无界 | 不丢弃,可能 OOM | SpscLinkedArrayQueue,分段扩容 |
onBackpressureBuffer(int maxSize) | maxSize | 延迟报 OverflowException(等价于 ERROR) | Queues.get 返回的 SPSC 队列 |
onBackpressureBuffer(int, Consumer) | maxSize | 同上,并回调被丢弃的元素 | 同上 |
onBackpressureBuffer(int, BufferOverflowStrategy) | maxSize | 按策略丢弃或报错 | ArrayDeque 子类,onNext 用 synchronized |
onBackpressureBuffer(int, Consumer, BufferOverflowStrategy) | maxSize | 按策略处理并回调 | 同上 |
onBackpressureBuffer(Duration, int, Consumer[, Scheduler]) | maxSize | 过期元素移出;缓冲满时丢最老 | 带 TTL 的队列 |
三个容易踩的点:
- 无界版本只是初始链路长度有限,队列本身可以一直增长,不能用「数据量应该不大」来兜底。
- 有界但不带策略的版本不是静默丢弃,而是报错;错误会等缓冲区排空后才送到下游。
- 带策略的版本内部是
ArrayDeque,不是无锁队列;onNext里有synchronized块。不要把它当成高并发场景的零开销结构。
三个策略的确定性实验
为了排除线程调度和「边生产边消费」的干扰,实验这样设计:
- 订阅时不请求任何元素;
Flux.range(1, 200)是同步源,会在subscribe()返回前把 200 个元素全部推给 buffer;subscribe()返回后,再逐个request(1)消费缓冲区。
这样溢出一定发生在消费之前,丢弃数量和保留元素都是确定的。
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.publisher.BufferOverflowStrategy;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class BufferStrategyDemo {
public static void main(String[] args) throws Exception {
for (BufferOverflowStrategy strategy : BufferOverflowStrategy.values()) {
run(strategy);
System.out.println();
}
}
static void run(BufferOverflowStrategy strategy) throws Exception {
List<Integer> received = new ArrayList<>();
AtomicInteger dropped = new AtomicInteger();
AtomicReference<Throwable> error = new AtomicReference<>();
CountDownLatch done = new CountDownLatch(1);
AtomicReference<Subscription> subscription = new AtomicReference<>();
Flux.range(1, 200)
.onBackpressureBuffer(10,
v -> dropped.incrementAndGet(), strategy)
.subscribe(new Subscriber<Integer>() {
@Override
public void onSubscribe(Subscription s) {
subscription.set(s); // 先不请求
}
@Override
public void onNext(Integer v) {
received.add(v);
}
@Override
public void onError(Throwable t) {
error.set(t);
done.countDown();
}
@Override
public void onComplete() {
done.countDown();
}
});
System.out.println(strategy + " after subscribe: dropped="
+ dropped.get() + " received=" + received.size()
+ " error=" + error.get());
for (int i = 0; i < 10; i++) {
subscription.get().request(1);
Thread.sleep(2);
}
subscription.get().request(100); // 触发完成/错误信号
done.await(2, TimeUnit.SECONDS);
System.out.println(strategy + " final: received=" + received
+ " dropped=" + dropped.get()
+ " error=" + error.get());
}
}
实测输出:
ERROR after subscribe: dropped=1 received=0 error=null
ERROR final: received=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] dropped=1 error=reactor.core.Exceptions$OverflowException: The receiver is overrun by more signals than expected (bounded queue...)
DROP_LATEST after subscribe: dropped=190 received=0 error=null
DROP_LATEST final: received=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] dropped=190 error=null
DROP_OLDEST after subscribe: dropped=190 received=0 error=null
DROP_OLDEST final: received=[191, 192, 193, 194, 195, 196, 197, 198, 199, 200] dropped=190 error=null
整理成表:
| 策略 | 第 11 个元素到来时 | 最终保留 | 丢弃数 | 终止信号 |
|---|---|---|---|---|
DROP_LATEST | 丢弃新来的 11 | 1–10 | 190(11–200) | onComplete |
DROP_OLDEST | 移除最老的 1,放入 11 | 191–200 | 190(1–190) | onComplete |
ERROR | 不放入,触发溢出错误 | 1–10 | 1(只回调 11) | 排空后 onError |
三个细节值得单独说:
- 钩子收到的元素不同。
DROP_LATEST的回调收到新来的元素(11–200);DROP_OLDEST收到被挤出的老元素(1–190);ERROR只回调一次,收到触发溢出的 11。 - ERROR 不是立即终止。缓冲里已有的 1–10 仍会被下游取走,队列排空后才收到
OverflowException。异常类型是reactor.core.Exceptions$OverflowException,它是IllegalStateException的子类,消息为The receiver is overrun by more signals than expected (bounded queue...)。 onBackpressureBuffer(int)与 ERROR 的行为一致,只是没有策略参数和可选的溢出回调;不要以为有界版本会自动丢弃。
队列实现与开销
| 版本 | 队列 | 并发访问 |
|---|---|---|
| 无界 | SpscLinkedArrayQueue | 单生产者单消费者,分段链表扩容 |
onBackpressureBuffer(int) | Queues.get 返回的 SpscArrayQueue | 单生产者单消费者,无锁 |
带 BufferOverflowStrategy | ArrayDeque 子类 | onNext 内 synchronized,溢出判断与插入是原子的 |
DROP_OLDEST 必须同时做「移除最老」和「放入最新」,这两步不能分开,否则并发下会丢错元素,所以实现里用了锁。它的吞吐上限和 SpscArrayQueue 不在一个量级,不要因为同为「有界 buffer」就假设性能一样。
什么时候用 Buffer
Buffer 是最后一道缓冲,不是限流手段。优先考虑这些做法:
| 需求 | 更合适的手段 |
|---|---|
| 控制并发 | flatMap(mapper, concurrency) |
| 控制请求节奏 | limitRate(n)、自定义 request(n) |
| 只要最新值 | onBackpressureLatest() |
| 允许丢一部分 | onBackpressureDrop() 或有界 buffer + 策略 |
| 处理慢消费者 | 扩容消费者、分流、降级,而不是加大队列 |
只有在下面这些条件同时成立时,才考虑 onBackpressureBuffer:
- 下游确实会以有限速率请求;
- 能给出明确的内存上限;
- 溢出时有可接受的业务语义(丢新、丢老或报错);
- 有丢弃计数、队列长度等监控指标。
用无界 buffer 省掉的复杂度,最终会以 OOM 或长尾延迟的形式还回来。
陷阱:Mono 没有 onBackpressureBuffer
// 编译错误:Mono 没有这个方法
Mono.just(1).onBackpressureBuffer();
onBackpressureBuffer 定义在 Flux 上,不在 Mono 上。Mono 最多只有一个元素,不需要用队列吸收需求差异;处理慢或失败应该用 Mono.delay、timeout、onErrorResume 等操作符。
小结
| 策略 | 行为 | 数据语义 | 风险 |
|---|---|---|---|
| 无界 | 无限缓冲 | 全保留 | OOM、长尾延迟 |
DROP_LATEST | 丢弃新元素 | 保留最早的 | 丢失新数据 |
DROP_OLDEST | 丢弃最老元素 | 保留最新的 | 丢失历史数据 |
ERROR | 排空后报错 | 终止前不丢 | 流不可用 |
三条结论:
onBackpressureBuffer向上游请求无界,它不抑制生产速度;下游不请求时才可能堆积。delayElements一次只请求 1 个元素,是慢消费者;buffer 放在它前面会持续积压。publishOn这类预取型操作符只在预取窗口内取走元素,窗口满后 buffer 同样会堆积。- 生产环境默认使用有界 buffer、明确的溢出策略和丢弃监控;无界 buffer 只能用于能证明上界的场景。
下一节:Drop Latest 与 Drop Oldest 的深层对比。
阅读导航
下一篇:7.2 背压策略:Drop Latest 与 Drop Oldest
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。