1. Overview
1.概述
When we’re building applications in a distributed cloud environment, we need to design for failure. This often involves retries.
当我们在分布式云环境中构建应用程序时,我们需要为失败进行设计。这通常涉及重试。
Spring WebFlux offers us a few tools for retrying failed operations.
Spring WebFlux为我们提供了一些重试失败操作的工具。
In this tutorial, we’ll look at how to add and configure retries to our Spring WebFlux applications.
在本教程中,我们将研究如何在Spring WebFlux应用程序中添加和配置重试。
2. Use Case
2.使用案例
For our example, we’ll use MockWebServer and simulate an external system being temporarily unavailable and then becoming available.
在我们的例子中,我们将使用MockWebServer,模拟一个外部系统暂时不可用,然后变得可用。
Let’s create a simple test for a component connecting to this REST service:
让我们为一个连接到这个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. Adding Retries
3.添加重试
There are two key retry operators built into the Mono and Flux APIs.
Mono和Flux API中内置了两个关键重试操作符。
3.1. Using retry
3.1.使用重试
First, let’s use the retry method, which prevents the application from immediately returning an error and re-subscribes a specified number of times:
首先,让我们使用retry方法,它可以防止应用程序立即返回错误,并重新订阅指定次数。
public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.bodyToMono(String.class)
.retry(3);
}
This will retry up to three times, no matter what error comes back from the web client.
这将重试最多三次,无论从网络客户端返回什么错误。
3.2. Using retryWhen
3.2.使用retryWhen
Next, let’s try a configurable strategy using the retryWhen method:
接下来,让我们试试使用retryWhen方法的可配置策略。
public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.max(3));
}
This allows us to configure a Retry object to describe the desired logic.
这允许我们配置一个Retry对象来描述所需的逻辑。
Here, we’ve used the max strategy to retry up to a maximum number of attempts. This is equivalent to our first example but allows us more configuration options. In particular, we should note that in this case, each retry happens as quickly as possible.
在这里,我们使用了max策略,重试的次数达到最大。这与我们的第一个例子相当,但允许我们有更多的配置选项。特别是,我们应该注意到,在这种情况下,每次重试都尽可能快地发生。
4. Adding Delay
4.添加延时
The main disadvantage of retrying without any delay is that this does not give the failing service time to recover. It may overwhelm it, making the problem worse and reducing the chance of recovery.
没有任何延迟的重试的主要缺点是,这没有给失败的服务以恢复的时间。它可能会使它不堪重负,使问题变得更糟,减少恢复的机会。
4.1. Retrying with fixedDelay
4.1.用fixedDelay重试
We can use the fixedDelay strategy to add a delay between each attempt:
我们可以使用fixedDelay策略,在每次尝试之间增加一个延迟。
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)));
}
This configuration allows a two-second delay between attempts, which may increase the chances of success. However, if the server is experiencing a longer outage, then we should wait longer. But, if we configure all delays to be a long time, short blips will slow our service down even more.
这种配置允许两次尝试之间有两秒的延迟,这可能会增加成功的机会。然而,如果服务器正在经历较长时间的中断,那么我们应该等待更长时间。但是,如果我们把所有的延迟都配置成一个很长的时间,短的突发事件会使我们的服务更加缓慢。
4.2. Retrying with backoff
4.2.用backoff重试
Instead of retrying at fixed intervals, we can use the backoff strategy:
我们可以使用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)));
}
In effect, this adds a progressively increasing delay between attempts — roughly at 2, 4, and then 8-second intervals in our example. This gives the external system a better chance to recover from commonplace connectivity issues or handle the backlog of work.
实际上,这增加了尝试之间的延迟–在我们的例子中,大概是2、4、然后8秒的间隔。这给了外部系统一个更好的机会来恢复常见的连接问题或处理积压的工作。
4.3. Retrying with jitter
4.3.用jitter重试
An additional benefit of the backoff strategy is that it adds randomness or jitter to the computed delay interval. Consequently, jitter can help to reduce retry-storms where multiple clients retry in lockstep.
backoff策略的另一个好处是,它在计算的延迟间隔中增加了随机性或jitter。因此,抖动可以帮助减少多个客户端同步重试的重试风暴。
By default, this value is set to 0.5, which corresponds to a jitter of at most 50% of the computed delay.
默认情况下,该值被设置为0.5,这相当于最多计算出的延迟的50%的抖动。
Let’s use the jitter method to configure a different value of 0.75 to represent jitter of at most 75% of the computed delay:
让我们使用jitter方法来配置一个不同的值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));
}
We should note that the possible range of values is between 0 (no jitter) and 1 (jitter of at most 100% of the computed delay).
我们应该注意,可能的数值范围在0(无抖动)和1(抖动最多为计算延迟的100%)之间。
5. Filtering Errors
5.过滤错误
At this point, any errors from the service will lead to a retry attempt, including 4xx errors such as 400:Bad Request or 401:Unauthorized.
此时,来自服务的任何错误将导致重试,包括4xx错误,如400:坏请求或401:未经授权。
Clearly, we should not retry on such client errors, as server response is not going to be any different. Therefore, let’s see how we can apply the retry strategy only in the case of specific errors.
显然,我们不应该在这种客户端错误上重试,因为服务器的响应不会有任何不同。因此,让我们看看如何只在特定错误的情况下应用重试策略。
First, let’s create an exception to represent the server error:
首先,让我们创建一个异常来表示服务器错误。
public class ServiceException extends RuntimeException {
public ServiceException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}
}
Next, we’ll create an error Mono with our exception for the 5xx errors and use the filter method to configure our strategy:
接下来,我们将创建一个错误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));
}
Now we only retry when a ServiceException is thrown in the WebClient pipeline.
现在我们只在WebClient管道中抛出ServiceException时重试。
6. Handling Exhausted Retries
6.处理用尽的重试
Finally, we can account for the possibility that all our retry attempts were unsuccessful. In this case, the default behavior by the strategy is to propagate a RetryExhaustedException, wrapping the last error.
最后,我们可以考虑一下所有重试都不成功的可能性。在这种情况下,策略的默认行为是传播一个RetryExhaustedException,包住最后一个错误。
Instead, let’s override this behavior by using the onRetryExhaustedThrow method and provide a generator for our ServiceException:
相反,让我们通过使用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());
}));
}
Now the request will fail with our ServiceException at the end of a failed series of retries.
现在,该请求将在一系列失败的重试结束后以我们的ServiceException失败。
7. Conclusion
7.结语
In this article, we looked at how to add retries in a Spring WebFlux application using retry and retryWhen methods.
在这篇文章中,我们研究了如何使用retry和retryWhen方法在Spring WebFlux应用程序中添加重试。
Initially, we added a maximum number of retries for failed operations. Then we introduced delay between attempts by using and configuring various strategies.
最初,我们为失败的操作增加了一个最大的重试次数。然后,我们通过使用和配置各种策略,引入了尝试之间的延迟。
Finally, we looked at retrying for certain errors and customizing the behavior when all attempts have been exhausted.
最后,我们研究了对某些错误的重试,以及当所有尝试都被用完时的自定义行为。
As always, the full source code is available over on GitHub.
一如既往,完整的源代码可在GitHub上获得,。