天天看点

spring-retry重试1.关于spring-retry2.用法3.原理

1.关于spring-retry

spring-retry是从spring batch独立出来的一个功能,主要实现了重试和熔断。

2013-3:【1.0.0】~2017-12:【1.2.2】

2.用法

1.pom引入

<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.1.5.RELEASE</version>
</dependency>      

依赖:spring-aop、spring-beans、spring-core等spring核心包

2.注解

@EnableRetry

在需要重试的类上增加,加载重试相关配置信息。其proxyTargetClass属性为true时,使用CGLIB代理。默认使用JDK代理。

@Retryable注解,被注解的方法发生异常时会重试

value:指定发生的异常进行重试

include:和value一样,默认空,当exclude也为空时,所有异常都重试

exclude:指定异常不重试,默认空,当include也为空时,所有异常都重试

maxAttemps:重试次数,默认3

backoff:重试补偿机制,默认没有

@Backoff重试补偿策略

delay:指定延迟后重试

multiplier:指定延迟的倍数,比如delay=5000l,multiplier=2时,第一次重试为5秒后,第二次为10秒,第三次为20秒

  • 不设置参数时,默认使用FixedBackOffPolicy,重试等待1000ms。
  • 只设置delay()属性时,使用FixedBackOffPolicy,重试等待指定的毫秒数。
  • 当设置delay()和maxDealy()属性时,重试等待在这两个值之间均态分布。
  • 使用delay(),maxDealy()和multiplier()属性时,使用ExponentialBackOffPolicy。
  • 当设置multiplier()属性不等于0时,同时也设置了random()属性时,使用ExponentialRandomBackOffPolicy
@Retryable(value = {SignServiceException.class}, maxAttempts = 3, backoff = @Backoff(delay = 5000l, multiplier = 2))
public void createContract(String serviceName, SignRecord signRecord, ContractParamData contractParamData) {
    this.signRecord = signRecord;
    SpringUtil.getBean(serviceName, CreateContractService.class).create(signRecord, contractParamData);
}      

@Recover注解,方法的参数为@Retryable异常类。当所有重试操作完成时(依然没有获取正确的结果)可以通过 RecoveryCallback实现一个兜底的操作(如发送邮件或短信提醒。需要注意的是发生的异常和入参类型一致时才会回调)。返回值应与重试方法返回相同。

@Recover
public void sendEmail(SignServiceException e) {
    log.error("生成合同重试 = {} ,error = {}", JSON.toJSONString(signRecord), e);
    emailService.sendWarnEmailContractError(signRecord.getOrderId(), "生成合同", e.getMessage());
}      

3.API

//模板
RetryTemplate template = new RetryTemplate();
//异常信息
Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>();
retryableExceptions.put(RetryException.class, true);
//重试策略:SimpleRetryPolicy固定次数重试策略
RetryPolicy retryPolicy = new SimpleRetryPolicy(4, retryableExceptions);
//重试等待策略:ExponentialBackOffPolicy 指数等待策略
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000L);//初始等待时间1S
backOffPolicy.setMultiplier(2D);//指数。重试一次后相乘
backOffPolicy.setMaxInterval(100000L);//最大等待时间
template.setRetryPolicy(retryPolicy);
template.setBackOffPolicy(backOffPolicy);
//执行
String result = template.execute(context -> {
    try {
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        System.out.println("第 " + index + " 次调用call方法:" + sf.format(new Date()));
        index++;
        throw new RetryException("异常信息");
    } catch (Exception e) {
        throw new RetryException("异常信息");
    }
}, context -> {
    SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    System.err.println("异常信息:" + context.getLastThrowable().getMessage());
    System.err.println("执行回调策略:" + sf.format(new Date()));
    return null;
});      

3.原理

RetryOperations:定义重试的API,RetryTemplate是API的模板模式实现,实现了重试和熔断,线程安全的。内部通过while循环,RetryContext存储重试上下文参数信息,根据RetryPolicy重试策略判断是否可以重试,通过RetryCallback的doWithRetry执行重试的动作,RetryListener监听并记录重试信息,最后在重试完成依然失败的情况下,执行RecoveryCallback定义的方法逻辑。

<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E;

<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback) throws E;

<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState) throws E, ExhaustedRetryException;

<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState retryState)throws E;

spring-retry重试1.关于spring-retry2.用法3.原理

RetryContext:重试上下文。存储重试相关的参数。重试次数、异常信息、超时时间等

RetryPolicy:重试策略。根据不同的策略判断是否执行重试操作。

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

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

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

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

CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate,稍后详细介绍该策略;

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

BackOffPolicy:补偿策略。根据不同策略判断下次执行重试的时间。

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:随机指数退避策略,引入随机乘数,之前说过固定乘数可能会引起很多服务同时重试导致DDos,使用随机休眠时间来避免这种情况。

RetryCallback:定义了需要执行重试的操作。

方法:T doWithRetry(RetryContext context) throws E;

RecoveryCallback:“兜底”回调。

方法:T recover(RetryContext context) throws Exception;

RetryListener:监听,统计记录重试的数据信息。异常信息

spring-retry通过AOP实现对目的方法的封装,执行在当前线程下,所以重试过程中当前线程会堵塞。如果BackOff时间设置比较长,最好起异步线程重试(也可以加@Async注解)。

@Service
@EnableRetry
@Slf4j
@EnableAsync
@Async(value = "taskExecutor")
public class RetryContractService {
    @Autowired
    EmailService emailService;

    private SignRecord signRecord;


    @Retryable(value = {SignServiceException.class}, maxAttempts = 3, backoff = @Backoff(delay = 5000l, multiplier = 2))
    public void createContract(String serviceName, SignRecord signRecord, ContractParamData contractParamData) {
        this.signRecord = signRecord;
        SpringUtil.getBean(serviceName, CreateContractService.class).create(signRecord, contractParamData);
    }

    @Recover
    public void sendEmail(SignServiceException e) {
        log.error("生成合同重试 = {} ,error = {}", JSON.toJSONString(signRecord), e);
        emailService.sendWarnEmailContractError(signRecord.getOrderId(), "生成合同", e.getMessage());
    }

}      

有状态重试 OR 无状态重试

所谓无状态重试是指重试在一个线程上下文中完成的重试,反之不在一个线程上下文完成重试的就是有状态重试。之前的SimpleRetryPolicy就属于无状态重试,因为重试是在一个循环中完成的。那么什么会后会出现或者说需要有状态重试呢?通常有两种情况:事务回滚和熔断。

如数据库操作异常DataAccessException,则不能执行重试,而如果抛出其他异常可以重试。

熔断的意思不在当前循环中处理重试,而是全局重试模式(不是线程上下文)。熔断会跳出循环,那么必然会丢失线程上下文的堆栈信息。那么肯定需要一种“全局模式”保存这种信息,目前的实现放在一个cache(map实现的)中,下次从缓存中获取就能继续重试了。

参考:

https://blog.csdn.net/broadview2006/article/details/72841056

https://blog.csdn.net/songhaifengshuaige/article/details/79441326

熔断器设计模式:http://blog.jobbole.com/75283/