
Spring WebFlux 的 RetryBackoffSpec 默认不支持运行时动态调整退避时长,但可通过自定义 Retry 实现——根据 HTTP 响应(如 Retry-After 头或自定义状态码 106 及其 retryTime 字段)实时计算重试延迟,兼顾服务端调度策略与客户端弹性。
spring webflux 的 `retrybackoffspec` 默认不支持运行时动态调整退避时长,但可通过自定义 `retry` 实现——根据 http 响应(如 `retry-after` 头或自定义状态码 106 及其 `retrytime` 字段)实时计算重试延迟,兼顾服务端调度策略与客户端弹性。
在实际微服务调用中,服务端常通过响应头(如 Retry-After)或响应体(如返回 JSON 中的 "retryTime": 30)主动告知客户端“建议重试时间”。此时,静态配置的 RetryBackoffSpec.backoff(5, ofSeconds(10)) 显得僵化——它无法感知服务端的实时限流决策,易造成无效重试或过早放弃。
为此,Spring Retry 提供了高度可扩展的 Retry 抽象:只需继承 Retry 并重写 generateCompanion(Flux<RetrySignal>) 方法,即可完全控制每次重试前的延迟逻辑。以下是一个生产就绪的动态重试实现,适配您提到的 HTTP 状态码 106 + retryTime 字段 场景:
private static class DynamicRetrySpec extends Retry {
private final int maxRetries;
private final Duration defaultBackoff;
public DynamicRetrySpec(int maxRetries, Duration defaultBackoff) {
this.maxRetries = maxRetries;
this.defaultBackoff = defaultBackoff;
}
@Override
public Publisher<?> generateCompanion(Flux<RetrySignal> retrySignals) {
return retrySignals.flatMap(this::computeDelay);
}
private Mono<Long> computeDelay(RetrySignal signal) {
Throwable failure = signal.failure();
if (failure instanceof WebClientResponseException webEx) {
// ✅ 匹配自定义状态码 106
if (webEx.getRawStatusCode() == 106) {
if (signal.totalRetries() >= maxRetries) {
log.warn("Max retries ({}) exhausted for status 106", maxRetries);
throw Exceptions.propagate(failure);
}
// ? 从响应体解析 retryTime(假设为 JSON {"retryTime": 45})
String body = webEx.getResponseBodyAsString();
try {
JsonNode json = new ObjectMapper().readTree(body);
int retrySeconds = json.path("retryTime").asInt(0);
if (retrySeconds > 0) {
Duration delay = Duration.ofSeconds(retrySeconds);
log.info("Retry #{} scheduled after {}s (from response body)",
signal.totalRetries(), delay.getSeconds());
return Mono.delay(delay).thenReturn(signal.totalRetries());
}
} catch (Exception e) {
log.warn("Failed to parse retryTime from response body: {}", body, e);
}
// ❌ fallback to default backoff if parsing fails
log.warn("Using default backoff {}s for status 106", defaultBackoff.getSeconds());
return Mono.delay(defaultBackoff).thenReturn(signal.totalRetries());
}
}
// ⚠️ 非106错误:仍可按需处理(如 429 Too Many Requests)
if (failure instanceof WebClientResponseException.TooManyRequests tooMany) {
String retryAfter = tooMany.getHeaders().getFirst("Retry-After");
if (retryAfter != null && !retryAfter.trim().isEmpty()) {
try {
long seconds = Long.parseLong(retryAfter.trim());
Duration delay = Duration.ofSeconds(seconds);
log.info("Retry #{} scheduled after {}s (from Retry-After header)",
signal.totalRetries(), delay.getSeconds());
return Mono.delay(delay).thenReturn(signal.totalRetries());
} catch (NumberFormatException ignored) {}
}
}
// ? 其他异常直接传播(不重试)
throw Exceptions.propagate(failure);
}
}使用方式(替换原 retryWhen):
Mono.fromCallable(() -> sendRequestToServer(params))
.doOnSuccess(result -> handleResponse(result, grantMapEntry, deviceInfo))
.retryWhen(new DynamicRetrySpec(3, Duration.ofSeconds(5))) // 最多重试3次,兜底5秒
.subscribe();⚠️ 关键注意事项:
- 响应体解析需谨慎:webEx.getResponseBodyAsString() 仅在 WebClient 配置了 codecs(...) 支持字符串解码时才有效;若未配置,需提前在 retrieve() 阶段 .bodyToMono(String.class) 捕获并透传。
- 线程安全:DynamicRetrySpec 是无状态的,可复用;但 ObjectMapper 实例建议使用单例(如 Spring 管理的 bean),避免频繁创建。
- 日志与监控:务必记录每次重试的实际延迟和来源(header/body/default),便于故障定位与容量评估。
- Jitter 支持:如需添加抖动(jitter),可在 Mono.delay(...) 前对 Duration 进行动态扰动,例如:Duration.ofSeconds((long)(baseSec * (1 + 0.5 * (Math.random() - 0.5))))。
通过该方案,客户端真正实现了“服务端驱动的智能重试”,既尊重服务治理策略,又避免了盲目的指数退避,显著提升系统整体鲁棒性与资源利用率。

















