7.3 背压策略:Error 与请求发射速率控制

讲清 onBackpressureError 的快速失败语义,以及 limitRate、limitRate(highTide, lowTide)、limitRequest 三种请求控制手段的真实行为。用 doOnRequest 实测 limitRate(10) 的请求序列 [10, 8, 8, ...] 与 75% 补请求规则,实测 limitRequest(5) 取满即完成、limitRequest(0) 不订阅上游,并给出 Flux.create + onRequest 的生产端限流写法。示例基于 reactor-core 3.8.8。

onBackpressureError:没有需求就终止

onBackpressureError 是最严格的策略:它在 onSubscribe 里向上游请求 Long.MAX_VALUE,但当元素到达而下游的请求计数为 0 时,不缓冲、不丢弃,直接发出 OverflowException 终止整条流。

用同步源和手动请求可以精确复现:

import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.publisher.Flux;

import java.util.ArrayList;
import java.util.List;

public class BackpressureErrorDemo {

    public static void main(String[] args) {
        List<Integer> received = new ArrayList<>();

        Flux.range(1, 200)
            .onBackpressureError()
            .subscribe(new Subscriber<Integer>() {
                @Override
                public void onSubscribe(Subscription s) {
                    // 故意不请求
                }

                @Override public void onNext(Integer v) { received.add(v); }

                @Override
                public void onError(Throwable t) {
                    System.out.println("received=" + received);
                    System.out.println("error=" + t.getClass().getName());
                    System.out.println("message=" + t.getMessage());
                }

                @Override public void onComplete() { }
            });
    }
}

实测输出:

received=[]
error=reactor.core.Exceptions$OverflowException
message=The receiver is overrun by more signals than expected (bounded queue...)

三个容易记错的点:

  • 异常类型不是 IllegalStateException。实际类型是 reactor.core.Exceptions$OverflowException,它继承 IllegalStateException;判断时应该用 Exceptions.isOverflow(t),而不是 catch (IllegalStateException e)。
  • 错误发生在第一个元素到达时,不是订阅时。如果上游一个元素都不发,这条流不会报错。
  • 它自己不保留任何元素。这与 onBackpressureBuffer(n, ..., BufferOverflowStrategy.ERROR) 不同:后者会先把队列里已有的元素发完,再在排空后报错;onBackpressureError 没有队列,直接终止。

一个常见误解是「delayElements 会无界消费,所以永远不会触发这个错误」。实际相反:delayElements 内部是 delayUntil,走不带预取的 concatMap,一次只请求 1 个元素。Flux.interval(10ms).onBackpressureError().delayElements(100ms) 在 550ms 内实测 consumed=0 errors=1——第一个延迟还没完成,下一个 interval 元素就到了,需求为 0,流立即失败。

onBackpressureError 适合「不能丢、也不能缓冲,宁可整条流失败」的场景:下游必须自己控制节奏,一旦失控就暴露出来。代价是失败后没有恢复点,需要外层 retryWhen 或告警介入。

limitRate:把上游请求切成批次

limitRate 经常被当成「限流操作符」,但它控制的是请求的批次大小,不是元素的处理速率。

它的实现很短:

// Flux.limitRate(int) 的内部实现
public final Flux<T> limitRate(int prefetchRate) {
    return onAssembly(this.publishOn(Schedulers.immediate(), prefetchRate));
}

也就是说,它插入了一个预取队列,把下游的请求攒成批再向上游要。publishOn 的默认补请求水位是预取量的 75%:

// Operators.unboundedOrLimit(10, 10)
10 - (10 >> 2) == 8

实测请求序列

import reactor.core.publisher.Flux;

import java.util.ArrayList;
import java.util.List;

public class LimitRateDemo {

    static List<Long> requests = new ArrayList<>();

    public static void main(String[] args) {
        Flux.range(1, 100)
            .doOnRequest(requests::add)
            .limitRate(10)
            .subscribe(v -> { });
        System.out.println("requests=" + requests);
    }
}

实测输出:

requests=[10, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8]

第一次是初始预取 10;之后每消费到 8 个(10 的 75%,向上取整为 8),就补请求 8。整个生命周期一共向上游请求了 10 + 12 × 8 = 106 个,但源只有 100 个元素——请求总量可以超过元素总量,limitRate 给的是上限,不是精确配额。

limitRate 不会替你产生下游需求

这是最容易踩的坑。下面这段代码只会收到 10 个元素,并且永远不会完成:

Flux.range(1, 100)
    .limitRate(10)
    .subscribe(new BaseSubscriber<Integer>() {
        @Override
        protected void hookOnSubscribe(Subscription s) {
            request(10);          // 只请求一次
        }

        @Override
        protected void hookOnNext(Integer v) {
            // 这里不再 request
        }
    });

实测 received=10 completed=false。limitRate 向上游补请求,只是让上游把元素送进它的预取队列;下游自己的请求额度用完,流就停在那里。subscribe() 默认请求 Long.MAX_VALUE,所以日常写法看不出问题;一旦换成 BaseSubscriber 手动请求,就必须继续 request。

反过来,如果下游逐个请求,limitRate 仍然按批向上游要:

Flux.range(1, 15)
    .doOnRequest(requests::add)
    .limitRate(10)
    .subscribe(new BaseSubscriber<Integer>() {
        @Override
        protected void hookOnSubscribe(Subscription s) {
            request(1);
        }

        @Override
        protected void hookOnNext(Integer v) {
            request(1);
        }
    });

实测 requests=[10, 8] received=15。下游一次只要 1 个,上游仍然先被要了 10 个、再补 8 个。多出来的元素放在 limitRate 的预取队列里,所以上游已生产但下游未消费的元素最多可达到预取量。这就是它和 onBackpressureBuffer 的区别:后者是显式的缓冲算子,前者是隐式的预取窗口。

limitRate(highTide, lowTide)

两参数版本把初始预取量和后续补请求量分开:

Flux.range(1, 30).doOnRequest(requests::add).limitRate(10, 2).subscribe();
// requests=[10, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]

Flux.range(1, 30).doOnRequest(requests::add).limitRate(10, 0).subscribe();
// requests=[10, 10, 10, 10]

规则来自 Operators.unboundedOrLimit(prefetch, lowTide):

  • lowTide <= 0:关闭提前补请求,只有预取队列排空时才请求 highTide;
  • lowTide >= highTide:退化为默认的 75% 策略;
  • 其他情况:每次消费 lowTide 个就补 lowTide 个。

lowTide 越小,请求与生产之间的并发窗口越大,补请求调用也越频繁。官方 javadoc 给过一个例子:下游总共请求 14 个,limitRate(10, 2) 会连续执行 7 次 request(2);默认 lowTide=8 时只需要一次 request(8)。

什么时候用 limitRate

典型场景是下游请求量很大,但数据源按小批次更高效:数据库分页、按批拉取的 API、需要控制单次网络往返的场景。它让上游每次最多看到 prefetchRate 个请求,而不是 Long.MAX_VALUE。

它不适合用来「让快生产者慢下来」:上游请求被切小了,但 limitRate 自己的预取队列仍会吸收差额,消费端真正处理不过来时,积压只是换了个地方。控制并发要用 flatMap(mapper, concurrency),控制总时长要用 timeout,控制采样频率要用 sample。

limitRequest:限制整个生命周期的总请求

limitRequest(n) 限制的是累计向上游请求的数量,而不是每次请求的大小。它在 3.8.8 里仍然存在,但已经标记为废弃:

// Flux.limitRequest(long) 的实现
@Deprecated
public final Flux<T> limitRequest(long n) {
    return take(n, true);
}

javadoc 建议新代码用 take(n, true) / take(n) 代替。它的实际行为是「取满 n 个后正常完成」:

Flux.range(1, 100)
    .doOnRequest(total::addAndGet)
    .limitRequest(5)
    .subscribe(values::add, e -> { }, () -> completed.set(true));

实测:

values=[1, 2, 3, 4, 5] completed=true totalRequested=5

不是「忽略后续元素」,也不是「把后续元素丢弃后继续运行」——它在第 5 个元素之后取消上游并发出 onComplete。

边界情况:limitRequest(0) 根本不订阅上游,直接完成。实测 subscribed=false completed=true(用 doOnSubscribe 观察)。

和 take(n) 的区别

take(n) 保证下游最多看到 n 个,但它依赖取消信号向上游传播;如果中间有 publishOn 这类预取操作符,上游可能在取消到达之前已经多生产了一批。limitRequest(n) 则从请求端封顶,上游累计最多只会被请求 n 个。

实测对比(异步竞态,具体数值每次会变,但趋势稳定):

Flux.range(1, 100)
    .doOnNext(v -> produced.incrementAndGet())
    .publishOn(Schedulers.parallel(), 64)
    .take(5)
    .blockLast();
// produced=27

Flux.range(1, 100)
    .doOnNext(v -> produced.incrementAndGet())
    .limitRequest(5)
    .publishOn(Schedulers.parallel(), 64)
    .take(5)
    .blockLast();
// produced=5

当上游生产本身有成本或副作用(发网络请求、查数据库、计费)时,这个差别很重要:take(5) 可能已经触发了 27 次生产,limitRequest(5) 则严格只触发 5 次。

注意 limitRequest 是总量上限,不是速率限制。要限制「每秒多少个」,应该用 sample、delayElements、timeout 或在入口做令牌桶,而不是 limitRequest。

Producer-side 与 Consumer-side

方式谁控制手段适用
Consumer-sideSubscriberrequest(n)标准 Reactive Streams 背压
Consumer-side 批量化操作符limitRate(n)减少 request 往返
Consumer-side 总量封顶操作符limitRequest(n) / take(n, true)限制上游总生产量
Producer-sidePublisherFlux.create + onRequest桥接回调式/拉取式数据源

生产端限流的正确写法是 Flux.create 配合 sink.onRequest:每次下游请求时,按请求量发射,而不是在循环里轮询 requestedFromDownstream()。

import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.publisher.Flux;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class ProducerRateDemo {

    public static void main(String[] args) {
        AtomicInteger emitted = new AtomicInteger();
        List<Integer> received = new ArrayList<>();

        Flux<Integer> flux = Flux.create(sink -> {
            sink.onRequest(n -> {
                System.out.println("onRequest n=" + n);
                for (long i = 0; i < n && !sink.isCancelled()
                        && emitted.get() < 5; i++) {
                    sink.next(emitted.incrementAndGet());
                }
                if (emitted.get() == 5) {
                    sink.complete();
                }
            });
        });

        flux.subscribe(new Subscriber<Integer>() {
            Subscription subscription;

            @Override
            public void onSubscribe(Subscription s) {
                subscription = s;
                s.request(2);
            }

            @Override
            public void onNext(Integer v) {
                received.add(v);
                if (received.size() == 2) {
                    subscription.request(3);
                }
            }

            @Override public void onError(Throwable t) { }
            @Override public void onComplete() { System.out.println("onComplete"); }
        });

        System.out.println("received=" + received);
    }
}

实测输出:

onRequest n=2
onRequest n=3
onComplete
received=[1, 2, 3, 4, 5]

三个实现要点:

  • n 是本次新增的请求量,直接按它发射即可。requestedFromDownstream() 返回的是内部计数快照,适合做诊断指标,不适合当作「还能发几个」的许可证;在重入请求的场景下,它和当前回调的 n 并不相等。
  • Flux.push 不适用这套写法。push 是纯推送模型,注册 onRequest 时会立刻收到 Long.MAX_VALUE,无法表达按需生产。需要背压就用 Flux.create。
  • 不要在订阅线程里 Thread.sleep 等需求。下面这种写法既阻塞了 subscribe(),又在 BUFFER 默认策略下悄悄把超量元素排进队列:
// 反面示例
Flux.create(sink -> {
    int count = 0;
    while (count < 100 && !sink.isCancelled()) {
        if (sink.requestedFromDownstream() > 0) {
            sink.next(count++);
        } else {
            Thread.sleep(10);      // 阻塞订阅线程
        }
    }
    sink.complete();
});

如果要主动从上游拉数据,正确做法是在 onRequest 回调里向上游 request(n),把需求传递给真正的数据源;而不是在本地循环里轮询计数器。

小结

手段控制对象关键行为
onBackpressureError()无需求时的单个元素立即 OverflowException,不缓冲
limitRate(n)向上游请求的批次初始 n,之后按 75% 补请求;不产生下游需求
limitRate(h, l)批次与补请求量l=0 只在队列排空时补;l>=h 退化为 75%
limitRequest(n)整个生命周期的总请求量取满 n 个后完成;n=0 不订阅上游
Flux.create + onRequest生产端发射节奏按回调参数 n 发射;Flux.push 不支持

记住三层区别:onBackpressureError 决定「没有需求时这个元素怎么办」;limitRate 决定「一次向上游要多少」;limitRequest 决定「一辈子最多要多少」。三者可以组合,但都不能替代对下游并发和处理能力的真实评估。

下一节:Cold Publisher 与 Hot Publisher。

阅读导航

上一篇:7.2 背压策略:Drop Latest 与 Drop Oldest

下一篇:8.1 Cold Publisher:每次订阅重放

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南