spring-retry重试

    xiaoxiao2022-07-04  125

    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;

     

    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/

    最新回复(0)