Hello everyone, I am the third child. I have been very busy recently. I haven’t written an original article for more than a month. I have sorted out an old manuscript and mixed it into one issue.
Everyone knows that Laosan is engaged in cross-border business. In cross-border business, third-party servers may be distributed in every corner of the world. Therefore, when requesting a third-party interface, you will inevitably encounter some network problems. At this time, you need to add heavy Now that we have tried the mechanism, I will share with you several ways to write interface retries in this issue.
Retry mechanism implementation
1. Loop retry
This is the simplest and most direct way. Add a loop to the code block of the request interface. If the request fails, continue the request until the request succeeds or the maximum number of retries is reached.
Sample code:
int retryTimes = 3; for(int i = 0; i < retryTimes; i + + ){<!-- --> try{<!-- --> // Code for requesting interface break; }catch(Exception e){<!-- --> // Handle exceptions Thread.sleep(1000); //Retry after 1 second delay } }
In this simple example code, a for
loop is directly used to retry, and the maximum number of retries is set to 3 times. At the same time, when an exception occurs, in order to avoid frequent requests, use Thread.sleep()
to add an appropriate delay.
2. Use recursive structures
In addition to loops, recursion can also be used to implement request retry of the interface. Recursion is a programming technique that we are all familiar with. It calls itself in the method of the request interface. If the request fails, it continues to be called until the request succeeds or the maximum number of retries is reached.
Sample code:
public void requestWithRetry(int retryTimes){<!-- --> if(retryTimes <= 0) return; try{<!-- --> // Code for requesting interface }catch(Exception e){<!-- --> // Handle exceptions Thread.sleep(1000); //Retry after 1 second delay requestWithRetry(retryTimes - 1); } }
In this code, we define a method named requestWithRetry
, where retryTimes
represents the maximum number of retries. If the number of retries is less than or equal to 0, return directly. Otherwise, after catching the exception, we use the Thread.sleep()
method to add an appropriate delay and then call itself to retry.
3. Use the built-in retry mechanism of network tools
Some of our commonly used HTTP clients usually have some built-in retry mechanisms, which only need to be configured when creating the corresponding client instance. Take Apache HttpClient
as an example:
- Version 4.5+: Use the
HttpClients.custom().setRetryHandler()
method to set the retry mechanism
CloseableHttpClient httpClient = HttpClients.custom() .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)) .build();
- Version 5.x: Use the
HttpClients.custom().setRetryStrategy()
method to set the retry mechanism
CloseableHttpClient httpClient = HttpClients.custom() .setRetryStrategy(new DefaultHttpRequestRetryStrategy(3,NEG_ONE_SECOND)) .build();
In the above sample code, we use DefaultHttpRequestRetryHandler
or DefaultHttpRequestRetryStrategy
to create a retry mechanism with a maximum number of retries of 3. If the request fails, it is automatically retried.
Apache HttpClient
also supports custom retry strategies, which can implement the HttpRequestRetryHandler
interface (version 4.5+) or the RetryStrategy
interface (version 5.x) , and implement retry logic as needed.
Here is an example of a custom retry policy:
CloseableHttpClient httpClient = HttpClients.custom() .setRetryStrategy((response, executionCount, context) -> {<!-- --> if (executionCount > 3) {<!-- --> // If the number of retries exceeds 3, give up retrying. return false; } int statusCode = response.getCode(); if (statusCode >= 500 & amp; & amp; statusCode < 600) {<!-- --> // If a server error status code is encountered, retry return true; } // No retry in other cases return false; }) .build();
4. Use Spring Retry library
When using the retry mechanism in a Spring project, you can use the Spring Retry library to implement it. Spring Retry provides a set of annotations and tool classes that can easily add retry functions to methods.
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>1.3.1</version> </dependency>
There are two ways to use Spring Retry. One is to use RetryTemplate to explicitly call methods that need to be retried, and the other is to use practical annotations to automatically trigger retries.
Explicitly use RetryTemplate
- Create a RetryTemplate object and configure the retry strategy:
RetryTemplate retryTemplate = new RetryTemplate(); //Configure retry strategy RetryPolicy retryPolicy = new SimpleRetryPolicy(3); retryTemplate.setRetryPolicy(retryPolicy); //Configure retry interval policy FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); backOffPolicy.setBackOffPeriod(1000); retryTemplate.setBackOffPolicy(backOffPolicy);
In the code, we created a RetryTemplate object and configured the retry strategy and retry interval strategy. SimpleRetryPolicy is used here to specify the maximum number of retries to 3 times, and FixedBackOffPolicy is used to specify the retry interval to 1 second.
- Use RetryTemplate to call the method:
retryTemplate.execute((RetryCallback<Void, Exception>) context -> {<!-- --> // Code for requesting interface return null; });
In the code, we use the retryTemplate.execute()
method to execute the code block that needs to be retried. In the doWithRetry()
method of RetryCallback
, you can write the logic that needs to be retried. If the method execution fails, RetryTemplate will retry according to the configured retry policy and retry interval policy.
Spring Retry is a library that provides a retry mechanism that can be easily used in Spring projects. Use the @Retryable annotation to mark methods that need to be retried. If the method throws an exception, it will be automatically retried.
@Retryable(value = Exception.class, maxAttempts = 3) public void request(){<!-- --> // Code for requesting interface }
Spring Retry provides a variety of retry strategies and retry interval strategies. We can choose the appropriate strategy according to specific business needs:
- Retry strategy:
- SimpleRetryPolicy: Specifies the maximum number of retries.
- TimeoutRetryPolicy: Specifies the maximum retry time.
- AlwaysRetryPolicy: Retry unconditionally.
- Retry interval strategy:
- FixedBackOffPolicy: Retry at fixed intervals.
- ExponentialBackOffPolicy: Exponentially increasing interval retries.
- UniformRandomBackOffPolicy: Retries at random intervals.
By configuring different retry policies and retry interval policies, retry behavior can be flexibly controlled. Spring Retry also provides custom retry strategies and retry interval strategies, which can be implemented by implementing the RetryPolicy
interface and the BackOffPolicy
interface respectively. spacing strategy.
Use annotations to call
In addition to explicit calls using RetryTemplate, Spring Retry also provides annotations to trigger retries.
- Configure the retry aspect:
@Configuration @EnableRetry public class RetryConfig {<!-- --> // Configure other beans }
In the code, we use the @Configuration
annotation to mark the class as a configuration class, and use the @EnableRetry
annotation to enable the retry function.
- Use the @Retryable annotation to mark methods that need to be retried:
@Retryable(maxAttempts = 3) public void request() {<!-- --> // Code for requesting interface }
We marked the request()
method with the @Retryable
annotation and specified the maximum number of retries to be 3.
- Call the marked method:
@Autowired private HttpService httpService; httpService.request();
It is even simpler to use in the SpringBoot project. Use the @EnableRetry annotation to enable the Spring Retry function, and add the @Retryable annotation on the method that needs to be retried.
Sample code:
@SpringBootApplication @EnableRetry // Enable Spring Retry function public class MyApplication {<!-- --> public static void main(String[] args) {<!-- --> SpringApplication.run(MyApplication.class, args); } } @Service public class MyService {<!-- --> @Retryable(value = {<!-- -->MyException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000)) public void doSomething() {<!-- --> //Method logic that needs to be retried } }
In the code, the @EnableRetry annotation enables the Spring Retry function. The @Retryable annotation marks the method that needs to be retried, and specifies the exception type, maximum number of retries, and retry interval.
Among them, the @Backoff annotation is used to specify the retry interval policy, and the delay attribute indicates the interval between each retry. In this example, the interval between each retry is 1 second.
It should be noted that the @Retryable annotation can only be marked on public methods. If you need to use the retry function on non-public methods, you can use the proxy pattern to implement it.
In addition, if you need to perform some specific operations during the retry process, such as logging, sending messages, etc., you can use the RetryContext parameter in the retry method, which provides some useful methods to obtain the context information of the retry. For example:
@Service public class MyService {<!-- --> @Retryable(value = {<!-- -->MyException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000)) public void doSomething(RetryContext context) {<!-- --> // Get the number of retries int retryCount = context.getRetryCount(); // Get the last exception Throwable lastThrowable = context.getLastThrowable(); // Record logs, send messages and other operations // ... //Method logic that needs to be retried } }
5. Use the Resilience4j library
Resilience4j is a lightweight, easy-to-use fault-tolerant library that provides multiple mechanisms such as retry, circuit breaker, and current limiting.
<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> <version>1.7.0</version> </dependency>
Let’s take a look at the use of Resilience4j. Resilience4j also supports explicit code calls and annotation configuration calls.
Explicitly called through code
-
Create a RetryRegistry object:
First, you need to create a RetryRegistry object to manage Retry instances. You can use the
RetryRegistry.ofDefaults()
method to create a default RetryRegistry object.
RetryRegistry retryRegistry = RetryRegistry.ofDefaults();
-
Configure Retry instance:
Next, a Retry instance can be created and configured through the RetryRegistry object. You can use the
RetryConfig
class to customize the configuration of Retry, including the maximum number of retries, retry interval, etc.
RetryConfig config = RetryConfig.custom() .maxAttempts(3) .waitDuration(Duration.ofMillis(1000)) .retryOnResult(response -> response.getStatus() == 500) .retryOnException(e -> e instanceof WebServiceException) .retryExceptions(IOException.class, TimeoutException.class) .ignoreExceptions(BusinessException.class, OtherBusinessException.class) .failAfterMaxAttempts(true) .build(); Retry retry = retryRegistry.retry("name", config);
Through the above code, we created a Retry instance named “name”, and configured the maximum number of retries to 3 times, the retry interval to 1 second, and retry when the status code of the returned result is 500. Retry when a WebServiceException exception is thrown, ignore BusinessException and OtherBusinessException exceptions, and throw a MaxRetriesExceededException exception after reaching the maximum number of retries.
-
Use Retry call:
Finally, you can use
Retry
to decorate and execute code blocks that need to be retried. For example, you can use theRetry.decorateCheckedSupplier()
method to decorate a Supplier that needs to be retried.
CheckedFunction0<String> retryableSupplier = Retry.decorateCheckedSupplier(retry, () -> {<!-- --> // Code that needs to be retried return "result"; });
Call via annotations
It is more concise to use Resilience4j to use the retry function through annotations.
In a Spring Boot project, you can use the @Retryable
annotation to mark methods that need to be retried.
@Service public class MyService {<!-- --> @Retryable(value = {<!-- -->MyException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000)) public void doSomething() {<!-- --> //Method logic that needs to be retried } }
In the code, the @Retryable
annotation marks the doSomething()
method, specifying the retry exception type as MyException.class
and the maximum number of retries. is 3 times, and the retry interval is 1 second.
6. Customized retry tool class
If we don’t want to introduce some additional retry frameworks into the project, we can also define a retry tool class ourselves. This is a set of retry tool classes I found in the client-sdk provided by a third party. It’s relatively lightweight, so I’d like to share it with you.
-
First, define a specific callback class that implements the
Callback
abstract class, and implement thedoProcess()
method to execute the logic that needs to be retried. ThedoProcess()
method of the callback class returns aRetryResult
object, representing the result of the retry.public abstract class Callback {<!-- --> public abstract RetryResult doProcess(); }
-
Then, define a
RetryResult
class to encapsulate the result of the retry. TheRetryResult
class contains aisRetry
attribute indicating whether a retry is required, and anobj
attribute indicating the result object of the retry.public class RetryResult {<!-- --> private Boolean isRetry; privateObject obj; // Constructor and getter methods omitted public static RetryResult ofResult(Boolean isRetry, Object obj){<!-- --> return new RetryResult(isRetry, obj); } public static RetryResult ofResult(Boolean isRetry){<!-- --> return new RetryResult(isRetry, null); } }
-
Finally, define a
RetryExecutor
class, in which theexecute()
method receives a number of retries and a callback object, and executes the callback object’sdoProcess cyclically based on the number of retries. ()
method until the maximum number of retries is reached or the callback object returns a result that does not require retries.public class RetryExecutor {<!-- --> public static Object execute(int retryCount, Callback callback) {<!-- --> for (int curRetryCount = 0; curRetryCount < retryCount; curRetryCount + + ) {<!-- --> RetryResult retryResult = callback.doProcess(); if (retryResult.isRetry()) {<!-- --> continue; } return retryResult.getObj(); } return null; } }
-
When using this custom retry tool class, you only need to implement a callback class inherited from
Callback
and implement specific retry logic in it. Then, perform the retry operation by calling theRetryExecutor.execute()
method. An anonymous implementation is used directly here://Maximum number of retries int maxRetryCount = 3; Object result = RetryExecutor.execute(maxRetryCount, new Callback() {<!-- --> @Override public RetryResult doProcess() {<!-- --> //Execute logic that requires retrying // If retry is needed, return RetryResult.ofResult(true) // If no retry is required, return RetryResult.ofResult(false, result) } });
7. Concurrency framework asynchronous retry
In some scenarios that require fast response, we can use the concurrency framework to implement asynchronous retries.
For example, use the thread pool ThreadPoolExecutor
to convert the request interface into an asynchronous task, put the task into the thread pool for asynchronous execution, and retry the request interface concurrently. You can judge the task execution result after the task execution is completed, and continue to try again if it fails.
int maxRetryTimes = 3; int currentRetryTimes = 0; ThreadPoolExecutor executor = new ThreadPoolExecutor( 10, //Number of core threads 10, //Maximum number of threads 0L, //Idle thread survival time TimeUnit.MILLISECONDS, // time unit new LinkedBlockingQueue<>() // Task queue ); Callable<String> task = () -> {<!-- --> // Code for requesting interface return "result"; }; Future<String> future; while (currentRetryTimes < maxRetryTimes) {<!-- --> try {<!-- --> future = executor.submit(task); String result = future.get(); // Determine the task execution result break; } catch (Exception e) {<!-- --> currentRetryTimes + + ; // Handle exceptions try {<!-- --> Thread.sleep(1000); } catch (InterruptedException ex) {<!-- --> Thread.currentThread().interrupt(); } } }
In this example, we directly use ThreadPoolExecutor
to create a thread pool, set the number of core threads and the maximum number of threads to 10, and use LinkedBlockingQueue
as the task queue. Then, we define a task of type Callable
to execute the code of the request interface. During the retry process, we use executor.submit(task)
to submit the task and obtain a Future
object through future.get()
Get the execution results of the task. If the task execution is successful, jump out of the loop; if the task execution fails, continue to retry until the maximum number of retries is reached.
8. Message queue retry
In some cases, we want to ensure the reliability of retries as much as possible, so that retry tasks will not be lost due to service interruptions. We can introduce message queues. We directly deliver the message to the message queue and implement the retry mechanism by consuming the message.
The sample code using RocketMQ is as follows:
@Component @RocketMQMessageListener(topic = "myTopic", consumerGroup = "myConsumerGroup") public class MyConsumer implements RocketMQListener<String> {<!-- --> @Override public void onMessage(String message) {<!-- --> try {<!-- --> // Code for requesting interface } catch (Exception e) {<!-- --> // Handle exceptions DefaultMQProducer producer = new DefaultMQProducer("myProducerGroup"); producer.setNamesrvAddr("127.0.0.1:9876"); try {<!-- --> producer.start(); Message msg = new Message("myTopic", "myTag", message.getBytes()); producer.send(msg); } catch (Exception ex) {<!-- --> // Handle sending exception } finally {<!-- --> producer.shutdown(); } } } }
In the above code, we use the @RocketMQMessageListener
annotation to mark the MyConsumer
class and specify the relevant configuration of the consumer, including the consumer group and subscribed topic.
In the onMessage()
method, we handle the logic of the request. If the request fails, we create a RocketMQ producer and resend the request to the message queue, waiting for the next processing.
By using a message queue (such as RocketMQ) to implement a retry mechanism, the reliability and stability of the system can be improved. Even if the service is interrupted, the retry task will not be lost, but will wait for the service to be restored to be processed again.
Best Practices and Considerations
When requesting retry, we should also pay attention to some key points to avoid causing more problems due to retry:
- Set the number of retries and the interval between retries reasonably to avoid sending requests frequently, and do not set too high a number of retries to avoid affecting system performance and response time.
- Consider interface idempotence: If the request is a write operation, and the downstream service does not guarantee the idempotence of the request, you need to be careful when retrying. You can retry in an idempotent way such as querying.
- During the retry process, concurrency issues need to be considered. If multiple threads retry at the same time, it may cause problems such as repeated requests or confusing request order. Locks or distributed locks can be used to solve concurrency problems.
- When handling exceptions, you need to handle them according to the specific exception type. Some exceptions can be solved by retrying, such as network timeout, connection exception, etc.; while some exceptions require special processing, such as database exception, file reading and writing exception, etc.
- When using the retry mechanism, you need to be careful not to fall into an infinite loop. If requests continue to fail and the number of retries continues to increase, it may lead to system crashes or resource exhaustion and other problems.
Reference:
[1]. “100 Common Mistakes in Java Business Development”
[2]. https://juejin.cn/post/7028947828248412168#heading-9
[3].https://resilience4j.readme.io/docs/retry