1. 概述
在構建分佈式雲環境中的應用程序時,我們需要設計容錯機制。這通常涉及重試。
Spring WebFlux 提供了幾個工具來處理操作失敗的情況。
在本教程中,我們將學習如何將重試添加到我們的 Spring WebFlux 應用程序中。
2. 用例
為了我們的示例,我們將使用 MockWebServer 並模擬外部系統暫時不可用,然後恢復可用狀態。
讓我們創建一個簡單的測試,用於測試一個連接到此 REST 服務的組件:
@Test
void givenExternalServiceReturnsError_whenGettingData_thenRetryAndReturnResponse() {
mockExternalService.enqueue(new MockResponse()
.setResponseCode(SERVICE_UNAVAILABLE.code()));
mockExternalService.enqueue(new MockResponse()
.setResponseCode(SERVICE_UNAVAILABLE.code()));
mockExternalService.enqueue(new MockResponse()
.setResponseCode(SERVICE_UNAVAILABLE.code()));
mockExternalService.enqueue(new MockResponse()
.setBody("stock data"));
StepVerifier.create(externalConnector.getData("ABC"))
.expectNextMatches(response -> response.equals("stock data"))
.verifyComplete();
verifyNumberOfGetRequests(4);
}3. 添加重試
Mono 和 Flux API 中內置了兩個關鍵的重試運算符。
3.1. 使用 retry
首先,我們使用retry方法,該方法可以防止應用程序立即返回錯誤並重試指定次數:
public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.bodyToMono(String.class)
.retry(3);
}它將重試最多三次,無論來自 Web 客户端的任何錯誤。
3.2. 使用 retryWhen 方法
接下來,讓我們嘗試使用可配置策略,通過 retryWhen 方法:
public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.max(3));
}這允許我們配置一個 重試 對象來描述所需的邏輯。
在這裏,我們使用了 max 策略,最多重試 N 次。 這與我們第一個示例相同,但允許我們更多的配置選項。 尤其需要注意的是,在這種情況下,每次重試都儘可能快。
4. 添加延遲
沒有延遲的情況下重試的主要缺點是,這不會給失敗的服務提供任何恢復時間。這可能會使其不堪重負,從而使問題更加嚴重,並降低恢復的可能性。
4.1. 使用固定延遲
我們可以使用 固定延遲 策略在每次嘗試之間添加延遲:
public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(2)));
}此配置允許在嘗試之間設置2秒的延遲,這可能會增加成功的機率。但是,如果服務器正在經歷更長時間的停機,我們應該等待更長時間。但是,如果我們配置所有延遲都非常長,那麼短暫的故障也會進一步降低我們的服務性能。
4.2. 使用回退策略 (Backoff)
與其以固定的時間間隔重試,我們可以使用回退策略:
回退策略 (Backoff) 是一種根據每次重試失敗自動調整重試間隔的策略。
public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(2)));
}實際上,這增加了嘗試之間的逐漸遞增延遲——在我們的示例中,大約在 2 秒、4 秒和 8 秒的間隔內。這為外部系統提供了更好的機會來從常見的連接問題中恢復,或處理工作積壓。
4.3. 使用抖動(jitter)重試
採用退避策略的額外優勢在於,它為計算的延遲間隔添加了隨機性或抖動。因此,抖動可以幫助減少重試風暴,即多個客户端同時以同步方式重試。
默認情況下,該值設置為 0.5,對應於計算延遲的 50% 以內的抖動。
讓我們使用抖動方法來配置不同的值為 0.75,以表示計算延遲的 75% 以內的抖動:
public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(2)).jitter(0.75));
}我們應該注意的是,可能的值範圍在 0 (無抖動) 和 1 (抖動最多為計算延遲的 100%) 之間。
5. 過濾錯誤
在這一階段,任何來自服務的錯誤都將導致重試嘗試,包括 4xx 錯誤,例如 400:請求無效 或 401:未授權。
顯然,我們不應該在遇到此類客户端錯誤時進行重試,因為服務器響應不會有任何不同。因此,讓我們看看如何 僅在特定錯誤情況下應用重試策略。
首先,讓我們創建一個異常來表示服務器錯誤:
public class ServiceException extends RuntimeException {
public ServiceException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}
}接下來,我們將創建一個錯誤處理程序 Mono,並將其與我們的異常結合起來,用於處理 5xx 錯誤,並使用 filter 方法來配置我們的策略:
public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.onStatus(HttpStatus::is5xxServerError,
response -> Mono.error(new ServiceException("Server error", response.rawStatusCode())))
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(5))
.filter(throwable -> throwable instanceof ServiceException));
}現在我們只在 WebClient 管道中拋出 ServiceException 異常時才重試。
6. 處理重試耗盡
最後,我們可以處理所有重試嘗試均未成功的情況。在這種情況下,策略的默認行為是傳播一個 RetryExhaustedException,並將其包裹住上一次的錯誤。
相反,我們可以通過覆蓋默認行為,使用 onRetryExhaustedThrow 方法並提供一個 ServiceException 的生成器來實現:
public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new ServiceException("Server error", response.rawStatusCode())))
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(5))
.filter(throwable -> throwable instanceof ServiceException)
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
throw new ServiceException("External Service failed to process after max retries", HttpStatus.SERVICE_UNAVAILABLE.value());
}));
}現在該請求將由於我們 ServiceException 在多次重試失敗系列末尾而失敗。
7. 結論
在本文中,我們探討了如何使用 <em >retry</em > 和 <em >retryWhen</em >> 方法在 Spring WebFlux 應用程序中添加重試機制。
最初,我們為失敗的操作設置了最大重試次數。然後,我們通過使用和配置各種策略,在嘗試之間引入延遲。
最後,我們研究了針對特定錯誤的重試以及在所有嘗試都耗盡時自定義行為。