5.3 错误处理模式


5.3 错误处理模式

本节摘要:响应式里错误是信号而非例外控制流。onError 终止当前流;onErrorResumeonErrorReturn 提供降级分支;retryWhen 把重试策略声明在流图上,便于监控与测试。

本节地图

  1. 区分 onErrorReturnonErrorResume
  2. retryWhen 实现指数退避
  3. 说明为何 try/catch 包不住异步 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。

核心回顾

  • 用算子表达降级,而非 imperative catch
  • 重试必须配退避与上限,防放大故障
  • StepVerifier 可断言 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 违反规范 一个流只接受一个终止

核心回顾


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U