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





































