bigywater.tech
Spring · 64

响应式的错误处理:onErrorResume、retry 与超时

响应式里出错之后流还能继续吗?onErrorResume 写在哪里差别多大?retry 为什么会重复发元素?timeout 限的是什么?为什么日志里会冒出 onErrorDropped?这篇用 Reactor 3.8.7 逐个验证。

[10, 20, -3, 40, 50]
恢复写在 flatMap 内层时的结果(写在外层则是 [10, 20, -1])
[1, 2, 1, 2, 1, 2, 3]
Flux 在第 3 个元素处失败后 retry(5) 的输出,前面的元素重复
513 ms
interval(100ms).take(5).timeout(300ms) 总共跑的时间,仍然正常完成(timeout 限元素间隔)

恢复

实测 · 本机执行输出
  不处理:[1, 2, ERROR(IllegalStateException: bad-3)]  用时 10 ms
  onErrorReturn(-1):[1, 2, -1, COMPLETE]  用时 0 ms
  onErrorResume(e -> Flux.just(100, 200)):[1, 2, 100, 200, COMPLETE]  用时 1 ms
  onErrorMap(包成 RuntimeException("wrapped", e)):[1, 2, ERROR(RuntimeException: wrapped:bad-3)]  用时 12 ms
  onErrorComplete():[1, 2, COMPLETE]  用时 0 ms
  onErrorContinue((e, v) -> 跳过出错的元素)+ map:[1, 2, 4, 5, COMPLETE]  用时 1 ms
  onErrorContinue 对 flatMap 里返回 Mono.error 的元素:[10, 20, 40, 50, COMPLETE]  用时 2 ms
  同样的 flatMap,不用 onErrorContinue:[10, 20, ERROR(IllegalStateException: bad-3)]  用时 0 ms

错误终止流;onErrorReturn / Resume 继续发;恢复写在内层只替换单个元素。

位置

实测 · 本机执行输出
  内层 onErrorResume(放在 flatMap 里面):[10, 20, -3, 40, 50, COMPLETE]  用时 0 ms
  外层 onErrorResume(放在 flatMap 后面):[10, 20, -1, COMPLETE]  用时 0 ms
  内层 onErrorResume 里再抛异常:[10, 20, ERROR(IllegalArgumentException: handler-failed)]  用时 1 ms

内层隔离单个元素的失败,外层整个流结束。

retry

实测 · 本机执行输出
  retry(5):[ok-on-attempt-3, COMPLETE]  用时 0 ms;上游被订阅 3 次,副作用执行 3 次
  retry(2) 一直失败:[ERROR(IllegalStateException: always-fail-3)]  用时 0 ms;上游被订阅 3 次
  Retry.max(2) 一直失败:[ERROR(RetryExhaustedException: Retries exhausted: 2/2)]  用时 7 ms;上游被订阅 3 次
  Retry.backoff(3, 100ms) 一直失败:[ERROR(RetryExhaustedException: Retries exhausted: 3/3)]  用时 809 ms;上游被订阅 4 次
  Retry.backoff(3, 100ms).jitter(0) 一直失败:[ERROR(RetryExhaustedException: Retries exhausted: 3/3)]  用时 720 ms;上游被订阅 4 次
  Retry.fixedDelay(3, 100ms).filter(e -> false):[ERROR(IllegalStateException: always-fail-1)]  用时 1 ms;上游被订阅 1 次
  Retry.backoff(3, 100ms).jitter(0).onRetryExhaustedThrow(原异常):[ERROR(IllegalStateException: always-fail-4)]  用时 725 ms;上游被订阅 4 次
  Flux 在第 3 个元素处失败,retry(5):[1, 2, 1, 2, 1, 2, 3, COMPLETE]  用时 0 ms;上游被订阅 3 次(前面的元素重复发出)

重新订阅整条上游,Flux 会重复发元素,backoff 耗尽抛 RetryExhaustedException。

timeout

实测 · 本机执行输出
  Mono.delay(300ms).timeout(100ms):[ERROR(TimeoutException: Did not observe any item or terminal signal within 100ms in 'source(MonoDelay)' (and no fallback has been configured))]  用时 105 ms
  Mono.delay(300ms).timeout(100ms, Mono.just(-1L)):[-1, COMPLETE]  用时 113 ms
  Flux.interval(100ms).take(5).timeout(300ms)(总共 500ms,每个间隔 100ms):[0, 1, 2, 3, 4, COMPLETE]  用时 513 ms
  Flux.concat(interval(100ms).take(3), Mono.delay(500ms)).timeout(300ms)(第 4 个迟到):[0, 1, 2, ERROR(TimeoutException: Did not observe any item or terminal signal within 300ms in 'source(FluxConcatArray)' (and no fallback has been configured))]  用时 614 ms
  超时会取消上游:[ERROR(TimeoutException: Did not observe any item or terminal signal within 100ms in 'peek' (and no fallback has been configured))]
      cancelled(上游收到取消信号)= true,阻塞里的 Thread.sleep 被中断 = true,任务正常跑完 = false
  timeout(100ms) 放在 retry 之前(每次尝试各自限时):[fast-3, COMPLETE]  用时 211 ms;上游被订阅 3 次
  timeout(100ms) 放在 retry 之后(整体限时):[ERROR(TimeoutException: Did not observe any item or terminal signal within 100ms in 'retry' (and no fallback has been configured))]  用时 111 ms;上游被订阅 1 次

Flux 上限制元素间隔;超时取消上游;与 retry 的顺序决定是每次限时还是整体限时。

被丢弃的错误

实测 · 本机执行输出
  timeout(100ms) 取消订阅之后,源头(Mono.create 的 sink)才报错:[ERROR(TimeoutException: Did not observe any item or terminal signal within 100ms in 'source(MonoCreate)' (and no fallback has been configured))]
      被 Hooks.onErrorDropped 收到的错误 = []
  Flux.merge(两个都会报错的源):[ERROR(IllegalStateException: A)]
      被 Hooks.onErrorDropped 收到的错误 = [](第二个错误去了哪里,这里没有看到)
  违反协议的源:onComplete 之后又 onError:[COMPLETE]
      被 Hooks.onErrorDropped 收到的错误 = [IllegalStateException: after-complete]
  没有自定义 hook 时的默认行为是在标准错误输出打一行 [ERROR] Operator called default onErrorDropped(见最后几行)
-- 标准错误里出现的 "Operator called default onErrorDropped" 日志共 2 行:
  [ERROR] (boundedElastic-1) Operator called default onErrorDropped - java.lang.InterruptedException: sleep interrupted
  [ERROR] (main) Operator called default onErrorDropped - java.lang.IllegalStateException: after-complete

取消之后的错误大多消失;违反协议的源才进 onErrorDropped。

分步演示

错误处理
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 reactor-core 3.8.7 的单个 Java 程序,不依赖 Spring 容器;用时的毫秒数会随运行抖动;第五节最后的两行 [ERROR] 日志来自标准错误输出
    • 没有验证:Mono.using / Flux.using 的资源清理与错误;doFinally 在错误和取消时的调用;checked exception 在 block() 里被包装成 ReactiveException 之外的细节;WebFlux 里 @ExceptionHandler 对响应式错误的处理(与第 43 篇的 MVC 做法对照);Hooks.onOperatorError;StepVerifier 测试写法;重试时 jitter 的随机范围
    京ICP备2026067502号-1京公网安备11010502063487号