本节摘要:响应式里错误是信号而非例外控制流。
onError终止当前流;onErrorResume、onErrorReturn提供降级分支;retryWhen把重试策略声明在流图上,便于监控与测试。
onErrorReturn 与 onErrorResumeretryWhen 实现指数退避flatMap 内部错误Reactive Streams:onError(t) 后禁止 onNext。Subscriber 必须停止 request 或转给全局 Hooks.onErrorDropped(不应依赖)。
Mono.just("ok") .flatMap(s -> failingRpc()) .onErrorResume(TimeoutException.class, e -> Mono.just(fallback())) .onErrorMap(e -> new ServiceException("wrapped", e));
响应式错误处理的第一原则:错误是流上的终止信号,不是可以随意捕获的异常。 在命令式代码里,try/catch 包住一段调用就能捕获同步错误;但在响应式里,flatMap 内部的异步调用发生在订阅之后、执行链的另一端,外层 try/catch 早已返回——错误只能通过 onError 信号沿流传播。因此所有错误处理都必须在流上声明:onErrorReturn 返回一个默认值、onErrorResume 切换到一个降级流、onErrorMap 转换错误类型。
| 算子 | 行为 | 适用 |
|---|---|---|
onErrorReturn(v) |
错误时发射默认值 v | 简单降级 |
onErrorReturn(class, v) |
仅匹配类型时降级 | 区分错误类型 |
onErrorResume(fn) |
错误时切换到新流 | 复杂降级逻辑 |
onErrorMap(fn) |
转换错误类型 | 统一错误契约 |
doOnError |
观察错误,不拦截 | 日志/监控 |
| 模式 | API | 注意 |
|---|---|---|
| 固定次数 | retry(3) |
不含退避 |
| 条件重试 | retryWhen(Retry.backoff(5, Duration.ofMillis(200))) |
Reactor 3.4+ |
| 无限危险 | retry() 无参 |
可能活锁 |
SOURCE 强调:retry(3) 是流定义的一部分,运维可从 metrics 看到重试次数,而非藏在 catch 块。
// 指数退避 + 上限:Retry.backoff Mono<String> resilient = rpcCall() .retryWhen(Retry.backoff(5, Duration.ofMillis(200)) .maxBackoff(Duration.ofSeconds(5)) // 退避上限 .filter(e -> !(e instanceof FatalException))); // 致命错误不重试
裸 retry() 的危险在于活锁:错误发生后立即重试,重试又立刻失败,形成一个无限快速循环,把系统拖入持续的错误风暴。Retry.backoff(maxAttempts, minBackoff) 用指数退避拉开重试间隔(200ms → 400ms → 800ms…),并允许通过 .filter 排除不可重试的错误类型(如参数错误、鉴权失败)。正确的重试策略 = 次数上限 + 退避间隔 + 错误类型过滤,三者缺一不可。
| 场景 | 退避 | 上限 | 过滤 |
|---|---|---|---|
| 数据库瞬时抖动 | 100ms 起步 | 3 次 | 排除 SQL 语法错 |
| 外部 RPC 超时 | 200ms 起步 | 5 次 | 排除 4xx |
| 慢查询优化 | 无(不重试) | 0 | — |
Resilience4j CircuitBreakerOperator 可包在 Reactor 链上:错误率超阈打开熔断,快速失败 onError 或降级 Mono。响应式 + 熔断 = 错误传播路径可编排。
// Resilience4j 熔断器包装响应式链 CircuitBreaker breaker = CircuitBreaker.ofDefaults("recommend"); Mono<User> protectedCall = Mono.defer(() -> recommend(userId)) .transformDeferred(CircuitBreakerOperator.of(breaker)) .onErrorResume(CircuitBreakerOpenException.class, e -> Mono.just(User.fallback())); // 熔断打开时快速降级
重试解决「暂时性故障」,熔断解决「持续故障」——两者必须组合:当错误率超过阈值,熔断器打开,后续请求快速失败而不是继续重试(避免放大故障);熔断器半开试探,成功则关闭。在响应式世界里,重试、熔断、降级全部以算子的形式挂在流上,让「故障应对策略」成为可监控、可测试的流定义的一部分——这正是第五章强调的「错误传播路径可编排」。
| 层级 | 策略 | 时机 |
|---|---|---|
| 单次请求 | timeout |
等待超限 |
| 短暂失败 | retryWhen |
可重试错误 |
| 持续失败 | CircuitBreaker | 错误率超阈 |
| 彻底失败 | onErrorResume 降级 |
以上都无效 |
⚠️ 常见坑:
subscribe()不传 error consumer,异常进入Schedulers.onHandleError线程,难关联 traceId。
expectError 与信号序列一句话总结:响应式错误处理 = 把「终止、恢复、节制」三件事全部声明在流上——onError 终止信号、onErrorResume/retry 恢复、熔断与退避节制,让故障成为可编排、可观测的流的一部分。
真实系统里错误处理通常是多层组合,而不是单层:
public Mono<OrderResult> placeOrder(Order order) { return orderRepo.save(order) // 第一层:等待预算 .timeout(Duration.ofMillis(500)) // 第二层:暂时性失败重试(指数退避,上限 3 次) .retryWhen(Retry.backoff(3, Duration.ofMillis(100)) .maxBackoff(Duration.ofSeconds(2)) .filter(e -> isTransient(e))) // 第三层:熔断保护(错误率过高时快速失败) .transformDeferred(CircuitBreakerOperator.of(breaker)) // 第四层:最终降级 .onErrorResume(e -> Mono.just(OrderResult.degraded(order.id, e))); }
每一层处理不同的故障维度:timeout 处理「慢」,retryWhen 处理「暂时性」,熔断处理「持续性」,onErrorResume 处理「兜底」。层次之间要分工清晰:如果 onErrorResume 把一切错误都降级掉,重试和熔断就永远没有机会触发——因为错误在上层就被消费了。调试这种多层链时,checkpoint(6.2)能帮你确认错误到底在哪一层被拦截。
| 误用 | 问题 | 正确做法 |
|---|---|---|
裸 retry() |
无限快速重试,活锁 | retryWhen + 退避 + 上限 |
onErrorReturn 兜所有 |
掩盖编程错误 | 按错误类型分流 |
| try/catch 包 flatMap | 捕获不到异步错误 | 用 onError 算子 |
| 在 doOnError 里恢复 | doOnError 不拦截错误 | 用 onErrorResume |
| 错误后继续 onNext | 违反规范 | 一个流只接受一个终止 |