重试框架入门:Spring-Retry&Guava-Retry

前言

在日常工作中,随着业务日渐庞大,不可避免的涉及到调用远程服务,但是远程服务的健壮性和网络稳定性都是不可控因素,因此,我们需要考虑合适的重试机制去处理这些问题,最基础的方式就是手动重试,侵入业务代码去处理,再高端一点的通过切面去处理,较为优雅的实现重试,下面,介绍两个重试框架,只需要配置好重启策略及重试任务,即可使用。

重试任务

这里只是模拟传参、相应及异常,具体任务需对应业务

package com.example.test.MessageRetry;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;

@Slf4j
public class RetryTask {
    /**
     * 重试方法
     * @param param
     * @return
     */
    public static boolean retryTask(String param){
        log.info("请求参数:{}",param);
        int i = RandomUtils.nextInt(0,15);
        log.info("随机数:{}",i);
        if(i == 0){
            log.error("参数异常");
            throw new IllegalArgumentException("参数异常");
        }else if(i > 10){
            log.error("访问异常");
            throw new RemoteAccessException("访问异常");
        }else if(i % 2 == 0){
            log.info("成功");
            return true;
        }else{
            log.info("失败");
            return false;
        }
    }
}

Spring-Retry

简介

Spirng-Retry是Spring提供的一个重试框架,为spring程序提供声明式重试支持,主要针对可能抛出异常的调用操作,进行有策略的重试。可以通过代码方式和注解方式实现,主要由重试执行者和两个策略构成:

  • RetryTemplet:重试执行者,可以设置重试策略(设置重试上线、如何重试)和回退策略(立即重试还是等待一段时间后重试,默认立即重试,如果需要配置等待一段时间后重试则需要指定回退策略),通过Execute提交执行操作,只有在调用时抛出指定配置的异常,才会执行重试
  • 重试策略:
策略 方式
NeverRetryPolicy 只允许调用RetryCallback一次,不允许重试
AlwaysRetryPolicy 允许无限重试,直到成功,此方式逻辑不当会导致死循环
SimpleRetryPolicy 固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
TimeoutRetryPolicy 超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
ExceptionClassifierRetryPolicy 设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
CircuitBreakerRetryPolicy 有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
CompositeRetryPolicy 组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行
  • 重试回退策略:
回退策略 方式
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 随机指数退避策略,引入随机乘数可以实现随机乘数回退
  • 此外,还需要配置重试时间间隔、最大重试次数以及可重试异常

实现

代码方式

RetryTemplate通过execute提交执行操作,需要准备RetryCallback和RecoveryCallback两个类实例,前者对应的就是重试回调逻辑实例,包装正常的功能操作,RecoveryCallback实现的是整个执行操作结束的恢复操作实例,只有在调用的时候抛出了异常,并且异常是在exceptionMap中配置的异常,才会执行重试操作,否则就调用到excute方法的第二个执行方法RecoveryCallback中

package com.example.test.MessageRetry;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;

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

@Service
@Slf4j
public class SpringRetryService {
    /**
     * 重试时间间隔ms,默认1000ms
     */
    private long retryPeriodTime = 5000L;
    /**
     * 最大重试次数
     */
    private int maxRetryNum = 3;
    /**
     * 哪些结果和异常要重试,key表示异常类型,value表示是否需要重试
     */
    private Map<Class<? extends Throwable>,Boolean> retryMap = new HashMap<>();

    @Test
    public void test(){
        retryMap.put(IllegalArgumentException.class,true);
        retryMap.put(RemoteAccessException.class,true);

        //构建重试模板
        RetryTemplate retryTemplate = new RetryTemplate();
        //设置重试回退操作策略,主要设置重试时间间隔
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(retryPeriodTime);
        //设置重试策略,主要设置重试次数
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryNum,retryMap);

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

        Boolean execute = retryTemplate.execute(
                //RetryCallback
                retryContext -> {
                    boolean b = RetryTask.retryTask("aaa");
                    log.info("调用结果:{}",b);
                    return b;
                },
                retryContext -> {
                    //RecoveryCallback
                    log.info("到达最多尝试次数");
                    return false;
                }
        );
        log.info("执行结果:{}",execute);
    }
}
注解方式

上面我们说到Spring-Retry是Spring提供的,那么,它就支持依赖整合

<!--spring-retry-->
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
            <version>1.2.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.1</version>
        </dependency>

然后,在启动类上添加开启注解

//表示是否开启重试,属性proxyTargetClass,boolean类型,是否创建基于子类(CGLIB)的代理,而不是标准的基于接口的代理,默认false
@EnableRetry

package com.example.test.MessageRetry;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;

@Slf4j
public class RetryTask {
    /**
     * 重试方法
     * @param param
     * @return
     */
    public static boolean retryTask(String param){
        log.info("请求参数:{}",param);
        int i = RandomUtils.nextInt(0,15);
        log.info("随机数:{}",i);
        if(i >-1){
            log.error("参数异常");
            throw new IllegalArgumentException("参数异常");
//        }else if(i > 10){
//            log.error("访问异常");
//            throw new RemoteAccessException("访问异常");
//        }else if(i % 2 == 0){
//            log.info("成功");
//            return true;
        }else{
            log.info("失败");
            return false;
        }
    }
}

/**
     * 重试调用方法
     * @param param
     * @return
     * @Retryable注解:
     *
     */
    @Retryable(value = {RemoteAccessException.class,IllegalArgumentException.class},maxAttempts = 3,backoff = @Backoff(delay = 5000L,multiplier = 2))
    public void call(String param){
        RetryTask.retryTask(param);
    }

    /**
     * 达到最大重试次数,或抛出了没有指定的异常
     * @param e
     * @param param
     * @return
     */
    @Recover
    public void recover(Exception e,String param){
        log.error("达到最大重试次数!!!!!");
    }

@Test
    public void retry(){
        springRetryService.call("aaa");
    }
  • @Retryable注解说明
属性 说明
value 指定重试的异常类型,默认为空
maxAttempts 最大尝试次数,默认3次
include 和value一样,默认为空,当exclude也为空时,默认所有异常
exclude 指定不处理的异常
Backoff 重试策略
  • @Backoff注解说明:设定重试倍数,每次重试时间是上次的n倍
属性 说明
delay 重试之间的等待时间(以毫秒为单位),默认0
maxDelay 重试之间的最大等待时间(以毫秒为单位),默认0
multiplier 延迟的倍数,默认0.0
delayExpression 重试之间的等待时间表达式,默认空
maxDelayExpression 重试之间的最大等待时间表达式,默认空
multiplierExpression 指定延迟的倍数表达式,默认空
random 随机指定延迟时间,默认false
  • @Recover注解说明:当重试到达指定次数时,将要回调的方法

  • @Retryable和@Recover修饰的方法要在同一个类中,且被@Retryable和@Recover标记的方法不能有返回值,这样Recover方法才会生效。由于@Retryable注解是通过切面实现的,因此我们要避免@Retryable 注解的方法的调用方和被调用方处于同一个类中,因为这样会使@Retryable 注解失效。

    我们可以看到,Spring-Retry只能针对指定异常重试,不能根据执行结果返回值重试,整体使用也比较死板,下面,看下更加灵活的Guava-Retry。

Guava-Retry

简介

Guava-Retry是谷歌的Guava库的一个小扩展,允许为任意函数调用创建可配置的重试策略,我们可以通过构建重试实例RetryBuilder,来设置重试源、配置重试次数、重试超时时间、等待时间间隔等,实现优雅的重试机制。

  • 主要属性
属性 说明
attemptTimeLimiter 时间限制策略,单次任务执行时间限制,超时终止
stopStrategy 停止重试策略
waitStrategy 等待策略
blockStrategy 任务阻塞策略,即当前任务执行完,下次任务执行前做什么,仅有线程阻塞threadSleepStrategy
retryException 重试异常(重试策略)
listeners 自定义重试监听器,可用于记录日志等
  • 时间限制策略
策略 说明
NoAttemptTimeLimit 对代理方法不添加时间限制,默认
FixedAttemptTimeLimit 对代理方法的尝试添加固定时间限制
  • 重试策略(重试异常)
策略 说明
retryIfException 抛出 runtime 异常、checked 异常时都会重试,但是抛出 error 不会重试
retryIfRuntimeException 只会在抛 runtime 异常的时候才重试,checked 异常和error 都不重试
retryIfExceptionOfType 允许我们只在发生特定异常的时候才重试,比如NullPointerException 和 IllegalStateException 都属于 runtime 异常,也包括自定义的error
retryIfResult 可以指定你的 Callable 方法在返回值的时候进行重试
  • 停止策略
策略 说明
StopAfterDelayStrategy 设定最长执行时间,无论任务执行几次,一旦超时,任务终止,返回RetryException
StopAfterAttemptStrategy 设定最大尝试次数,一旦超过,返回重试异常
NeverStopStrategy 一直轮询直到获取期望结果
  • 等待策略
策略 说明
ExceptionWaitStrategy 异常时长等待,如果抛出的是指定异常,则从传入的方法中取得等待时间并返回;如果异常不匹配,则返回等待时间为0L
CompositeWaitStrategy 复合时长等待,在获取等待时间时会获取多种等待策略各自的等待时间,然后累加这些等待时间
FibonacciWaitStrategy 斐波那契等待策略
ExponentialWaitStrategy 指数等待时长,指数增长,若设置了最大时间,则停止,否则到Long.MAX_VALUE
IncrementingWaitStrategy 递增等待,提供一个初始时长和步长,随次数叠加
RandomWaitStrategy 随机等待时长,可以提供一个最大和最小时间,从范围内随机
FixedWaitStrategy 固定等待时长

代码

<!--guava-retryer-->
        <dependency>
            <groupId>com.github.rholder</groupId>
            <artifactId>guava-retrying</artifactId>
            <version>2.0.0</version>
        </dependency>

package com.example.test.MessageRetry;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;

@Slf4j
public class RetryTask {
    /**
     * 重试方法
     * @param param
     * @return
     */
    public static boolean retryTask(String param){
        log.info("请求参数:{}",param);
        int i = RandomUtils.nextInt(0,15);
        log.info("随机数:{}",i);
        if(i < 3){
            log.error("参数异常");
            throw new IllegalArgumentException("参数异常");
        }else if(i > 10){
            log.error("访问异常");
            throw new RemoteAccessException("访问异常");
        }else if(i % 2 == 0){
            log.info("成功");
            return true;
        }else{
            log.info("失败");
            return false;
        }
    }
}

package com.example.test.MessageRetry;

import com.github.rholder.retry.*;
import com.google.common.base.Predicates;
import lombok.extern.slf4j.Slf4j;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class GuavaRetryService {
   
    public void guavaRetry(){
        //构建重试实例RetryBuilder,可以设置重试源,可以配置重试次数、重试超时时间、等待时间间隔
        Retryer<Boolean>retryer = RetryerBuilder.<Boolean>newBuilder()
                //设置异常重试源
                .retryIfExceptionOfType(RemoteAccessException.class)
                .retryIfExceptionOfType(IllegalArgumentException.class)
                //设置根据结果重试  res->res==false  Predicates.containsPattern("_error$")
                .retryIfResult(Predicates.equalTo(false))
                //设置等待时间
                .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
                //设置最大重试次数
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                //设置重试监听,可用作重试时的额外动作
                .withRetryListener(new RetryListener() {
                    @Override
                    public <V> void onRetry(Attempt<V> attempt) {
                        log.error("第【{}】次调用失败",attempt.getAttemptNumber());
                    }
                })
                //设置阻塞策略
                .withBlockStrategy(BlockStrategies.threadSleepStrategy())
                //设置时间限制
                .withAttemptTimeLimiter(AttemptTimeLimiters.noTimeLimit())
                .build();
        try{
            retryer.call(()->RetryTask.retryTask("aaa"));
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

可以看到,我们设置了重试三次,超过这个限制没有执行成功,抛出了重试异常,而且也可以根据我们的返回结果来判断。

总结

Spring-Retry和Guava-Retry都是线程安全的重试框架,能够保证并发业务下重试逻辑的正确性。两者都很好的将正常方法和重试方法进行了解耦,可以设置超时时间、重试次数、间隔时间、监听结果等,相比来说,Guava-Retry比Spring-Retry更加灵活,并且可以通过返回值来进行重试,两者都是非常好的重试框架,具体的选用看相关的业务场景即可。

相关推荐
打码人的日常分享1 分钟前
企业人力资源管理,人事档案管理,绩效考核,五险一金,招聘培训,薪酬管理一体化管理系统(源码)
java·数据库·python·需求分析·规格说明书
27669582922 分钟前
京东e卡滑块 分析
java·javascript·python·node.js·go·滑块·京东
爱写代码的刚子4 分钟前
C++知识总结
java·开发语言·c++
冷琴199611 分钟前
基于java+springboot的酒店预定网站、酒店客房管理系统
java·开发语言·spring boot
daiyang123...37 分钟前
IT 行业的就业情况
java
Nightselfhurt1 小时前
Spring cloud 中gateway原理
spring·spring cloud·gateway
爬山算法1 小时前
Maven(6)如何使用Maven进行项目构建?
java·maven
.生产的驴1 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
爱学的小涛1 小时前
【NIO基础】基于 NIO 中的组件实现对文件的操作(文件编程),FileChannel 详解
java·开发语言·笔记·后端·nio
吹老师个人app编程教学1 小时前
详解Java中的BIO、NIO、AIO
java·开发语言·nio