There are 8 ways to write interface request retries. Which one do you use?

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

8 retry mechanism implementations

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

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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

  1. 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();
  1. 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.

  1. Use Retry call:

    Finally, you can use Retry to decorate and execute code blocks that need to be retried. For example, you can use the Retry.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 the doProcess() method to execute the logic that needs to be retried. The doProcess() method of the callback class returns a RetryResult 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. The RetryResult class contains a isRetry attribute indicating whether a retry is required, and an obj 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 the execute() method receives a number of retries and a callback object, and executes the callback object’s doProcess 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 the RetryExecutor.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