強悍的重試機制:Spring Boot 中 WebClient 彈性設計實戰
環境:SpringBoot3.4.2
1. 簡介
Spring WebFlux 包含一個用于執行 HTTP 請求的客戶端。WebClient 具有基于 Reactor 的功能性流暢 API,它可以聲明式地組成異步邏輯,而無需處理線程或并發問題。它是完全無阻塞的,支持流式傳輸。
在分布式系統中,網絡請求可能因臨時故障(如超時、服務不可用、限流)而失敗。合理的重試機制能提升系統韌性,但不同 HTTP 客戶端的實現方式差異顯著。RestTemplate 和 RestClient 都需要通過自定義攔截器(ClientHttpRequestInterceptor)或是借助 Spring Retry 庫實現重試機制,開發者需手動處理異常類型、重試次數、退避策略等細節,代碼冗余且易出錯。而 WebClient 內置了響應式重試機制,通過 retryWhen 操作符與 RetryBackoffSpec 組合,可聲明式地定義重試規則,無需編寫攔截器或引入額外依賴。這種設計不僅簡化了代碼,還天然適配異步非阻塞場景
接下來,我們將詳細的介紹WebClient的重試機制。
2.實戰案例
2.1 基本使用
默認情況下,WebClient 實例不會自動執行任何重試操作,除非你主動添加相關操作。當你調用其他服務時,如果發生超時或拋出錯誤,請求會直接失敗,并將錯誤沿響應式鏈(reactive chain)傳遞下去。若要實現重試功能,你需要在數據流中添加一個重試操作符(retry operator)。
最直接的重試方式是使用 .retry(n) 方法,其中 n 表示首次失敗后允許的最大重試次數。如下示例:
// 基本配置
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000);
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:9999")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build() ;
// 調用遠程調用
webClient.get()
.uri("/api/query")
.retrieve()
.bodyToMono(String.class)
.doOnError(err -> {
System.err.printf("發生錯誤: %s%n", err.getMessage()) ;
})
.retry(2)
.subscribe(System.out::println, error -> System.err.printf("請求失敗: %s%n", error.getMessage()));這種方法添加簡單,但存在一個問題:它會立即連續重試,不給遠程系統任何恢復時間。對于偶發的網絡抖動(network flukes),這種策略可能有效;但如果問題是由服務過載(heavy load)引起的,連續重試反而可能加劇系統壓力。
注意:每次重試都會在前一次請求剛結束時立即啟動。這意味著,如果被調用的服務已經處于高負載狀態,這種連續重試只會進一步加劇系統壓力。總計3次。
圖片
下面是關于retry方法執行原理:
圖片
2.2 重試添加退避規則
重試調用在它們之間留有間隔時效果會更好。再次嘗試前給系統一個短暫的時間。這個間隔可以保持不變,也可以每次逐漸延長。我們可以使用 Retry.backoff 與 retryWhen 結合,這樣能做更多控制權,并且更符合 Reactor 處理重試的方式。并且可以決定暫停多久以及允許嘗試多少次。如下示例:
WebClient webClient = ... ;
webClient.get()
.uri("/api/query")
.retrieve()
.bodyToMono(String.class)
.doOnError(err -> {
System.err.printf("發生錯誤: %s - %s%n", DateTimeFormatter.ofPattern("mm:ss")
.format(LocalDateTime.now()), err.getMessage()) ;
})
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
.subscribe(System.out::println, error -> System.err.printf("請求失敗: %s%n", error.getMessage()));輸出結果
圖片
首次重試間隔為1s,之后是3s。這也給了目標服務恢復的時間。
2.3 自定義重試機制
并非所有失敗都值得重試。有些錯誤是暫時的,有些則無法自行恢復。例如:
- 404 表示請求的資源不存在
- 400 通常表示請求格式錯誤
這類錯誤無需重試,因為重復嘗試只會得到相同結果。但 500 服務器錯誤 或 超時 可能意味著服務只需多一秒即可恢復,此時重試才有意義。
通過 Retry 構建器,你可以配置一個過濾器,明確指定哪些失敗需要重試、哪些應直接跳過。如下示例:
Retry retryStrategy = Retry.fixedDelay(2, Duration.ofMillis(500))
// 過濾,值對500以上的錯誤碼進行重試
.filter(throwable -> {
if (throwable instanceof WebClientResponseException) {
int status = ((WebClientResponseException) throwable).getStatusCode().value() ;
return status >= 500;
}
return throwable instanceof WebClientRequestException;
}) ;
webClient.get()
.uri("/api/query")
.retrieve()
.bodyToMono(String.class)
.doOnError(err -> {
System.err.printf("發生錯誤: %s - %s%n", DateTimeFormatter.ofPattern("mm:ss")
.format(LocalDateTime.now()), err.getMessage()) ;
})
.retryWhen(retryStrategy)
.subscribe(System.out::println, error -> System.err.printf("請求失敗: %s%n", error.getMessage()));當發生小于500錯誤時,輸出如下:
圖片
當發生大于等于500錯誤時,輸出如下:
圖片
2.4 避免重試風暴
重試機制若使用不當易引發 "重試風暴":當大量服務無延遲地連續重試失敗請求時,會向已承壓的下游系統爆發式涌入流量,導致其崩潰并擴散至整個系統。避免風暴的關鍵在于退避(Backoff)與抖動(Jitter):退避通過逐步延長重試間隔降低負載,抖動則引入隨機性防止請求周期性對齊。如下示例:
Retry retryWithJitter = Retry.backoff(4, Duration.ofMillis(500))
.jitter(0.8)
.filter(throwable -> {
if (throwable instanceof WebClientResponseException) {
int status = ((WebClientResponseException) throwable).getStatusCode().value() ;
return status >= 500;
}
return throwable instanceof WebClientRequestException;
}) ;
webClient.get()
.uri("/api/query")
.retrieve()
.bodyToMono(String.class)
.doOnError(err -> {
System.err.printf("發生錯誤: %s - %s%n", DateTimeFormatter.ofPattern("mm:ss")
.format(LocalDateTime.now()), err.getMessage()) ;
})
.retryWhen(retryWithJitter)
.subscribe(System.out::println, error -> System.err.printf("請求失敗: %s%n", error.getMessage()));輸出結果
圖片
此處設置的抖動范圍為 80%,即每次重試的延遲時間會在基準值基礎上隨機增減一定比例。這種微小的時序偏移能避免所有重試請求在同一時刻集中涌向后端系統。
2.5 重試最終兜底 - 熔斷
當重試徹底失效時,需立即停止請求以避免系統雪崩,此時熔斷器(Circuit Breaker)便派上用場。它會持續監測失敗次數,一旦達到閾值,便臨時阻斷新請求,為下游服務爭取恢復時間。
Spring Boot 的 WebClient 本身未內置熔斷功能,但可無縫集成 Spring Cloud Circuit Breaker 或 Resilience4j。通過熔斷器包裝 WebClient 邏輯,可在故障持續時提前攔截請求,防止其涌向已崩潰的下游系統。如下示例:
首先,引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
<version>3.3.0</version>
</dependency>配置文件
resilience4j:
circuitbreaker:
# 此種方式適用于使用CircuitBreakerFactory方式
configs:
order-service:
minimum-number-of-calls: 1
failure-rate-threshold: 10
wait-duration-in-open-state: 10s示例代碼
private final ReactiveCircuitBreakerFactory<?, ?> rcbFactory ;
public Mono<String> invoke() {
return this.rcbFactory.create("order-service").run(webClient.get()
.uri("/api/query")
.retrieve()
.bodyToMono(String.class)
.doOnError(err -> {
System.err.printf("發生錯誤: %s - %s%n", DateTimeFormatter.ofPattern("mm:ss")
.format(LocalDateTime.now()), err.getMessage()) ;
}), ex -> Mono.just("fallback response")) ;
}運行結果
圖片




































