Spring重试机制实现方式

Spring重试机制实现方式

Spring Retry模块是Spring框架提供的一个用于实现重试逻辑的工具,它可以帮助开发者轻松地为可能失败的操作添加重试机制。

声明式重试

Spring Retry支持通过注解(如 @Retryable)以声明的方式定义重试逻辑,这样可以将重试逻辑从业务逻辑中分离出来,使代码更加简洁和易维护。

添加依赖

pom.xml 中添加 spring-retry 相关依赖:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

启用Spring-retry

在Spring Boot主应用类或者任意配置类上添加 @EnableRetry注解,启用Spring Retry功能:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;

@EnableRetry
@Configuration
public class SpringRetryConfig {

}

创建重试服务

定义一个服务类,在需要重试的方法上添加 @Retryable 注解。注解内可以配置重试的次数、重试间隔、异常类型等参数

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Retryable(
        value = { MyCustomException.class },
        maxAttempts = 5,
        backoff = @Backoff(delay = 2000))
    public void performOperation() throws MyCustomException {
        // 模拟可能失败的操作
        System.out.println("尝试执行操作...");
        if (Math.random() < 0.7) {
            throw new MyCustomException("操作失败,重试中...");
        }
        System.out.println("操作成功!");
    }

    @Recover
    public void recover(MyCustomException e) {
        // 当重试次数用尽后的处理逻辑
        System.out.println("所有重试均已失败,处理异常: " + e.getMessage());
    }
}

class MyCustomException extends Exception {
    public MyCustomException(String message) {
        super(message);
    }
}

@Retryable 注解参数说明

  • value、include: 接收一个 Throwable 的Class数组,指定哪些异常发生时应该触发重试机制。默认情况下,所有未被捕获的运行时异常都会触发重试。
  • exclude: 接收一个 Throwable 的Class数组,指定哪些异常不会触发重试,即使这些异常在 include 中添加了也不会触发。
  • maxAttempts: 定义最大重试次数,默认值通常是3次。
  • backoff: 定义一个重试策略,通过这个重试策略,可以重新指定重试之间的间隔时间、最大等待时间等信息。
  • listener: 指定一个或多个监听器类,用来监听重试过程中的事件(如开始、成功、失败)。可以用于记录日志或其他监控需求。
  • lable: 为 @RetryableTask 注解添加一个唯一的标识,方便在日志中识别重试任务。
  • stateful: boolean 类型,默认为 false。当为 true 时,表示重试过程中会保留状态,下次重试时,会继续使用上次的状态。需要注意的是,这里记录的是 Spring 的上下文状态,而不是任务本身状态。
  • interceptor: 用于指定一个或多个拦截器(Interceptor),这些拦截器可以在重试机制的各个阶段介入,以执行额外的逻辑。拦截器可以用来扩展重试行为,比如添加日志记录、性能监控、自定义业务逻辑等。
  • maxAttemptsExpression: 定义一个表达式,用于动态的计算最大重试次数。
  • recover: 指向一个恢复方法,该方法将在所有的重试都失败后被调用。恢复方法通常用于处理最终失败的情况,比如记录错误信息或者执行备选方案。

编程式重试

编程式重试会替换声明式重试中的 @Retryable 注解,使用代码的形式配置相关重试逻辑,以下提供一个简要的编程式重试实现过程。对 添加依赖启用重试 两部分不再重复说明。

配置重试模板类

使用编程式重试,需要手动配置重试相关逻辑,主要通过创建 RetryTemplate 实现。在这个类中,提供了关于重试策略的相关配置方式。可以将上面提到的 启用spring-retry 部分内容整合到这个配置类中。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import java.util.Collections;

@EnableRetry
@Configuration
public class RetryConfig {

    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();

        // 设置重试策略
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(5);
        retryTemplate.setRetryPolicy(retryPolicy);

        // 设置退避策略
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(2000);  // 设置重试间隔为2秒
        retryTemplate.setBackOffPolicy(backOffPolicy);

        return retryTemplate;
    }
}

创建重试服务

由于使用的是编程式重试,因此,在重试服务中需要替换掉前面的 @Retryable 注解,使用编码实现。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Autowired
    private RetryTemplate retryTemplate;

    public void performOperation() {
        try {
            retryTemplate.execute(new RetryCallback<Void, MyCustomException>() {
                @Override
                public Void doWithRetry(RetryContext context) throws MyCustomException {
                    // 模拟可能失败的操作
                    System.out.println("尝试执行操作...");
                    if (Math.random() < 0.7) {
                        throw new MyCustomException("操作失败,重试中...");
                    }
                    System.out.println("操作成功!");
                    return null;
                }
            });
        } catch (MyCustomException e) {
            System.out.println("所有重试均已失败,处理异常: " + e.getMessage());
        }
    }
}

class MyCustomException extends Exception {
    public MyCustomException(String message) {
        super(message);
    }
}

动态配置重试机制

在实际的开发编码过程中,可能需要根据不同的业务接口配置自定义的重试服务。因此,对于上面的重试配置,还需要提供更灵活的配置。假设系统中触发重试的异常、重试次数、间隔时间都是需要根据不同的接口配置文件动态配置的,在这种情况,需要通过编程方式动态创建 RetryTemplate对象,以支持不同接口的重试配置。由于 RetryTemplate 需要根据不同的接口动态定制,所以就不能再将 RetryTemplate 添加到spring容器中管理。假设接口的重试配置如下:

retry:
  service1:
    exceptions:
      - com.example.MyCustomException
      - com.example.AnotherCustomException
    maxAttempts: 5
    backoffPeriod: 2000
  service2:
    exceptions:
      - com.example.ThirdCustomException
    maxAttempts: 3
    backoffPeriod: 1000

配置绑定

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Map;

@Configuration
@ConfigurationProperties(prefix = "retry")
public class RetryProperties {

    private Map<String, RetryConfig> services;

    public Map<String, RetryConfig> getServices() {
        return services;
    }

    public void setServices(Map<String, RetryConfig> services) {
        this.services = services;
    }

    public static class RetryConfig {
        private List<String> exceptions;
        private int maxAttempts;
        private long backoffPeriod;

        public List<String> getExceptions() {
            return exceptions;
        }

        public void setExceptions(List<String> exceptions) {
            this.exceptions = exceptions;
        }

        public int getMaxAttempts() {
            return maxAttempts;
        }

        public void setMaxAttempts(int maxAttempts) {
            this.maxAttempts = maxAttempts;
        }

        public long getBackoffPeriod() {
            return backoffPeriod;
        }

        public void setBackoffPeriod(long backoffPeriod) {
            this.backoffPeriod = backoffPeriod;
        }
    }
}

重试服务

import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class RetryService {

    private final RetryProperties retryProperties;

    public RetryService(RetryProperties retryProperties) {
        this.retryProperties = retryProperties;
    }

    public void performOperation(String serviceName) {
        RetryProperties.RetryConfig config = retryProperties.getServices().get(serviceName);
        if (config == null) {
            throw new IllegalArgumentException("Invalid service name: " + serviceName);
        }

        RetryTemplate retryTemplate = createRetryTemplate(config);

        try {
            retryTemplate.execute(new RetryCallback<Void, Exception>() {
                @Override
                public Void doWithRetry(RetryContext context) throws Exception {
                    // 模拟可能失败的操作
                    System.out.println("尝试执行操作...");
                    if (Math.random() < 0.7) {
                        throw new MyCustomException("操作失败,重试中...");
                    } else if (Math.random() < 0.9) {
                        throw new AnotherCustomException("另一个异常,重试中...");
                    }
                    System.out.println("操作成功!");
                    return null;
                }
            });
        } catch (Exception e) {
            System.out.println("所有重试均已失败,处理异常: " + e.getMessage());
        }
    }

    private RetryTemplate createRetryTemplate(RetryProperties.RetryConfig config) {
        RetryTemplate retryTemplate = new RetryTemplate();

        // 动态加载异常类
        Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>();
        for (String exceptionName : config.getExceptions()) {
            try {
                Class<? extends Throwable> exceptionClass = (Class<? extends Throwable>) Class.forName(exceptionName);
                retryableExceptions.put(exceptionClass, true);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException("Class not found: " + exceptionName, e);
            }
        }

        // 设置重试策略
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(config.getMaxAttempts(), retryableExceptions);
        retryTemplate.setRetryPolicy(retryPolicy);

        // 设置退避策略
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(config.getBackoffPeriod());
        retryTemplate.setBackOffPolicy(backOffPolicy);

        return retryTemplate;
    }
}

此时,不再通过spring管理 RetryTemplate ,需要在每次调用的时候都通过代码创建一个新的对象。在生产项目中,也可以将下面的 createRetryTemplate 方法保存到一个专门的工具类中,通过更多的参数控制实现更灵活的创建策略。同时,Service类本身是业务处理类,从单一职责原则的角度来讲,它也不应该提供创建重试模板的相关逻辑。因此,生产最佳实践更推荐将创建重试模板相关的逻辑提取到单独的配置类中。

这里通过配置文件简单的配置了部分属性,对于实际更复杂的业务逻辑,可以通过配置实现更多的参数配置,这里不再赘述。