2021年10月30日关于springBoot的重试机制

1,657 阅读9分钟

背景

和公司内部对接rpc接口,可能存在调用失败的情况,此时就需要重新重试,保证业务顺利进行,重试失败需要对失败结果进行处理。

Spring的重试机制

Spring Retry 为应用程序提供了重试机制,针对客户调用过程中发生的异常进行重试处理。而不需要再去手动处理。

依赖

我自己新建了一个spring boot工程。依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!--引入spring boot-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>org.example</groupId>
    <artifactId>WorkTest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--tomcat集成-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>

        <!--dubbo依赖,可以让程序一直运行,不会启动后就停止-->
        <dependency>
            <groupId>com.alibaba.boot</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>0.1.0</version>
        </dependency>

        <!--spirng boot @SpringBootApplication等启动自注解依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.3.9.RELEASE</version>
        </dependency>


        <!--Java常用工具类依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.3.1</version>
        </dependency>

        <!--Spring Retry 重试机制依赖-->
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
            <version>1.2.2.RELEASE</version>
        </dependency>

        <!--lombo工具依赖-》log、Data等-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--测试依赖-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>
    </dependencies>

    <!--maven插件-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>

        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>application.yml</include>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
    </build>

</project>

启动类实现

启动类:

package com.anzhi.worktest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @Package: com.anzhi.worktest
 * @ClassName: WorkTestApplication
 * @Author: AZ
 * @CreateTime: 2021/11/7 17:20
 * @Description:
 */
@SpringBootApplication(scanBasePackages = "com.anzhi.worktest.*")
public class WorkTestApplication {
    public static void main(String[] args) {
        System.setProperty("workTest", "workTest");
        SpringApplication.run(WorkTestApplication.class, args);
    }
}

测试工具类

实现一个随机工具类根据不同的数字抛出不同异常

@Slf4j
public class RetryDemoTaskUtil {

    /**
     * 重试方法
     * @param param
     * @return
     */
    public static boolean retyrTask(String param){
        log.info("收到请求参数: {}", param);

        int i = RandomUtils.nextInt(0, 11);
        log.info("随机生成的参数: {}", i);
        if(i == 0){
            log.info("为0, 抛出参数异常");
            throw new IllegalArgumentException("参数异常");
        }else if(i ==1){
            log.info("为1, 返回true");
            return true;
        }else if(i == 2){
            log.info("为2,返回false");
            return false;
        }else{
            // 为其他
            log.info("大于2, 抛出自定义异常");
            throw new RemoteAccessException("大于2, 抛出远程访问异常");
        }
    }
}

测试类

@Slf4j
public class SpringRetryTemplateTest {

    /**
     * 重试时间间隔
     */
    private long fixedPeriodTime = 1000L;

    /**
     * 最大重试次数, 默认为3
     */
    private int maxRetryTimes = 3;

    /**
     * 表示那些异常需要重试, key表示异常的字节码, value为true表示需要重试
     */
    private Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();

    @Test
    public void testRetry(){

        // 设置重试回退策略, 主要设置重试时间间隔
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(fixedPeriodTime);

        // 设置重试策略,主要设置重试次数, 以及什么异常进行重试
        exceptionMap.put(RemoteAccessException.class, true);
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);

        // 构建重试模板实列
        RetryTemplate retryTemplate = new RetryTemplate();

        retryTemplate.setRetryPolicy(retryPolicy);
        retryTemplate.setBackOffPolicy(backOffPolicy);

        Boolean excute = retryTemplate.execute(
                retryContext -> {
                    boolean b = RetryDemoTaskUtil.retyrTask("abc");
                    log.info("调用结果: {}", b);
                    return b;
                },
                retryContext -> {
                    log.info("已经达到最大重试次数或抛出了不重试的异常");
                    return false;
                });
        log.info("执行结果: {}", excute);
    }

}

运行结果:

19:08:09.740 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=0
19:08:09.747 [main] INFO com.anzhi.worktest.Retry.util.RetryDemoTaskUtil - 收到请求参数: abc
19:08:09.754 [main] INFO com.anzhi.worktest.Retry.util.RetryDemoTaskUtil - 随机生成的参数: 0
19:08:09.755 [main] INFO com.anzhi.worktest.Retry.util.RetryDemoTaskUtil - 为0, 抛出参数异常
19:08:09.758 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=1
19:08:09.758 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry failed last attempt: count=1
19:08:09.758 [main] INFO com.anzhi.worktest.Retry.demo.SpringRetryTemplateTest - 已经达到最大重试次数或抛出了不重试的异常
19:08:09.758 [main] INFO com.anzhi.worktest.Retry.demo.SpringRetryTemplateTest - 执行结果: false

观察结果可以看到有自动重试

分析

RetryTemplate承担了重试任务执行者的角色。包含了SimpleRetryPolicy——重试策略(设置重试上限,重试的根源实体),FixedBackOffPolicy——固定的回退策略(设置执行重试回退的时间间隔)

RetryTemplate通过excute提交执行操作,需要准备RetryCallback 和RecoveryCallback 两个类实例,前者对应重试回调逻辑实例,包含正常的逻辑操作。后者实现的是整个重试机制执行完的操作实例

重试策略

重试策略也分多种,不单单只有一种


NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试

AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环

SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略

TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试

ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试

CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate

CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

重试回退策略

大白话讲就是失败你要立即做出处理还是等一会儿处理。Spring Retry默认是立即重试。如果需要配置等待一段时间则需要指定回退策略BackoffRetryPolicy。

NoBackOffPolicy:无退避算法策略,每次重试时立即重试

FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒

UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒

ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier

ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数可以实现随机乘数回退

以上是基于Spring-Retry 的普通使用方式。还有基于 Spring Boot 集成的使用方式。使用注解。

Spring Retry注解方式

另一种比较简单的重试机制是基于注解的方式

添加依赖:

<!--Spring Retry 重试机制依赖-->
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.2.RELEASE</version>
</dependency>

初始化打印

切面打印日志

@Slf4j
@SpringBootTest(classes = WorkTestApplication.class)
@RunWith(SpringRunner.class)
public class SpringRetryAnnotation {

    @Before
    public void init(){
        log.info("-------------测试开始-----------------");
    }


    @After
    public void after(){
        log.info("-------------测试结束-----------------");
    }
}

Service层测试

Service层代码,调用重试的逻辑方法。使用注解实现重试机制

@Service
@Slf4j
public class SpringRetryAnnoService {

    @Retryable(value = {RemoteAccessException.class, IllegalArgumentException.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000L, multiplier = 2))
    public boolean call(String param){
        return RetryDemoTaskUtil.retyrTask("abc");
    }

    @Recover
    public boolean recover(Exception e, String param){
        log.error("达到最大重试次数,或抛出了一个没有指定进行重试的异常 {}", e);
        return false;
    }
}

测试类

@Slf4j
public class SpringRetryAnnoTest extends SpringRetryAnnotation {

    @Autowired
    private SpringRetryAnnoService springRetryAnnoService;

    @Test
    public void retyr(){
        boolean result = springRetryAnnoService.call("abc");
        log.info("----结果是: {} --", result);
    }
}

测试结果:

2021-11-08 22:55:45.484  INFO 20332 --- [main] c.a.w.r.common.SpringRetryAnnotation     : -------------测试开始-----------------
2021-11-08 22:55:45.529  INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil       : 收到请求参数: abc
2021-11-08 22:55:45.534  INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil       : 随机生成的参数: 3
2021-11-08 22:55:45.535  INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil       : 大于2, 抛出自定义异常
2021-11-08 22:55:47.549  INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil       : 收到请求参数: abc
2021-11-08 22:55:47.550  INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil       : 随机生成的参数: 3
2021-11-08 22:55:47.550  INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil       : 大于2, 抛出自定义异常
2021-11-08 22:55:51.552  INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil       : 收到请求参数: abc
2021-11-08 22:55:51.552  INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil       : 随机生成的参数: 6
2021-11-08 22:55:51.552  INFO 20332 --- [main] c.a.w.retry.util.RetryDemoTaskUtil       : 大于2, 抛出自定义异常
2021-11-08 22:55:51.563 ERROR 20332 --- [main] c.a.w.r.server.SpringRetryAnnoService    : 达到最大重试次数,或抛出了一个没有指定进行重试的异常 {}

Guava-Retry重试框架

依赖

<!--重试框架之Guava-Retry依赖-->
<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>

重试方法:

@Slf4j
public class RetryGuavaDemoTask {
    /**
     * 重试方法
     */
    public static boolean retryTask(String param){
        log.info("收到请求参数: {}", param);

        int result = RandomUtils.nextInt(0, 11);
        log.info("随机生成的数: {}", result);
        if(result < 2){
            log.info("小于2, 抛出参数异常");
            throw new IllegalArgumentException("参数异常");
        }else if(result < 5){
            log.info("小于5, 返回true");
            return true;
        }else if(result < 7){
            log.info("小于7, 返回false");
            return false;
        }else{
            log.info("大于7, 抛出自定义异常");
            throw new RemoteAccessException("大于7, 抛出自定义异常");
        }
    }
}

测试方法

@Slf4j
public class GuavaRetryTest {

    @Test
    public void fun01(){
        // RetryerBuilder 构建重试实例 retryer,可以设置重试源且可以支持多个重试源,可以配置重试次数或重试超时时间,
        // 以及可以配置等待时间间隔
        Retryer<Boolean> retryer = RetryerBuilder.<Boolean> newBuilder()
                .retryIfExceptionOfType(RemoteAccessException.class) //设置异常重试源
                .retryIfResult(res -> res == false) //设置根据结果重试
                .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) // 设置等待时间间隔
                .withStopStrategy(StopStrategies.stopAfterAttempt(3)) //设置最大重试次数
                .build();
        try {
            retryer.call(() -> RetryGuavaDemoTask.retryTask("abc"));
        }catch (Exception e){
            log.info("重试异常: {}", e);
        }
    }
}

分析

指定异常:

  1. RetryBuilder 是一个Factory创建者,可以定制设置重试源并且可以支持多个重试源,可以配置重试次数和超时时间,以及可以配置等待时间间隔,创建重试者Retryer实例。

  2. RetryerBuilder 的重试源支持Exception异常对象和自定义断言对象,通过retryIfException和retryIfResult设置,同时支持多个且能兼容

  3. retryIfException: 抛出runtime异常、checked异常时都会重试,但是抛出error不会重试

  4. retryIfRuntimeException: retryIfRuntimeException 只会在抛 runtime 异常的时候才重试,checked 异常和error 都不重试。

  5. retryIfExceptionOfType: retryIfExceptionOfType 允许我们只在发生特定异常的时候才重试,比如NullPointerException 和 IllegalStateException 都属于 runtime 异常,也包括自定义的error。

如: retryIfExceptionOfType(Error.class)// 只在抛出error重试; 或者通过Predicate实现: .retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class),

Predicates.instanceOf(IllegalStateException.class)))

根据返回结果重试

  1. RetryifResult: 可以根据Callable方法的返回值进行重试

如上述代码所示:


.retryIfResult(res -> res == false) //设置根据结果重试
retryer.call(() -> RetryGuavaDemoTask.retryTask("abc"));'

RetryListener

当发生重试之后,假如需要做一些额外的处理动作,比如log异常,那么可以使用RetryListener。 每次重试后,guava-retrying会自动回调注册的监听。可以注册多个RetryListener,会按照注册顺序一次调用。如:

.withRetryListener(new RetryListener {      
 @Override    
   public <T> void onRetry(Attempt<T> attempt) {  
               logger.error("第【{}】次调用失败" , attempt.getAttemptNumber());  
          } 
 }) 

主要接口

接口描述备注
Attemp执行一次任务
AttemptTimeLimiter单次任务执行时间限制如果单次任务执行超时,则终止执行当前任务
BlockStrategies任务阻塞策略通俗的讲就是当前任务执行完,下次任务还没开始这段时间做什么),默认策略为:BlockStrategies.THREAD_SLEEP_STRATEGY
RetryException重试异常
RetryListener自定义重试监听器可以用于异步记录错误日志
StopStrategy停止重试策略
WaitStrategy等待时长策略(控制时间间隔),返回结果为下次执行时长

StopStrategy 停止策略

提供三种:

  1. StopAfterDelayStrategy: 设定一个最长允许的执行时间;比如设定最长执行10s,无论任务执行次数,只要重试的时候超出了最长时间,则任务终止,并返回重试异常RetryException;

  2. NeverStopStrategy: 不停止,用于需要一直轮训知道返回期望结果的情况;

  3. StopAfterAttemptStrategy: 设定最大重试次数,如果超出最大重试次数则停止重试,并返回重试异常;

WaitStrategy等待策略

  1. FixedWaitStrategy: 固定等待时长策略;

  2. RandomWaitStrategy: 随机等待时长策略(可以提供一个最小和最大时长,等待时长为其区间随机值)

  3. IncrementingWaitStrategy: 递增等待时长策略(提供一个初始值和步长,等待时间随重试次数增加而增加)

  4. ExponentialWaitStrategy: 指数等待时长策略;

  5. ExceptionWaitStrategy: 异常时长等待策略;

  6. CompositeWaitStrategy: 复合时长等待策略;

总结

spring-retry 和guava-retry 工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性。但是可以很明显的可以看出guava-retry使用具有更高的灵活性,能根据方法返回值来判断是否重试,而Spring-retry只能根据抛出的异常来进行重试。

Spring-Retry 使用注意事项

  1. 异常类型需要与Recover方法参数类型保持一致,且重试方法第一个参数必须为Throwable或其子类,否则找不到重试回调的方法;
  2. recover方法返回值需要与重试方法返回值保证一致

参考文章:

blog.csdn.net/zzzgd_666/a… blog.csdn.net/ryo10607324… blog.csdn.net/qq_35152236… blog.csdn.net/qq_39914581…