文章目录
retry: 英
/ˌriːˈtraɪ/
美/ˌriːˈtraɪ/
概述
在调用第三方接口或者使用Mq时,会出现网络抖动,连接超时等网络异常,所以需要重试。
网络抖动:标识一个网络的稳定性。抖动越小,网络越稳定。
Spring Retry是从
Spring Batch 2.2.0
版本独立出来的一个功能,主要实现了重试和熔断
。在 Spring Retry需要
指定触发重试的异常类型
,并设置每次重试的间隔
以及如果重试失败是继续重试还是熔断(停止重试)。
对于
重试是有场景限制的,不是什么场景都适合重试
,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。远程调用超时、网络突然中断可以重试- 在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。
一.简单实现重试
- 异常捕获
- 循环重试(包括:重试次数,重试间隔)
publicclassTestRetry{//最大重试次数privatestaticfinal Integer tryTimes=6;//重试间隔时间单位秒privatestaticfinal Integer intervalTime=1;publicstaticvoidmain(String[] args)throws InterruptedException{boolean flag= TestRetry.retryBuss();
System.out.println("最终执行结果:"+(flag?"成功":"失败"));}publicstaticbooleanretryBuss()throws InterruptedException{
Integer retryNum=1;boolean flag=false;while(retryNum<= tryTimes){try{
flag=execute(retryNum);if(flag){
System.out.println("第"+ retryNum+"次执行成功!!!");break;}
System.err.println("第"+ retryNum+"次执行失败...");
retryNum++;}catch(Exception e){
retryNum++;
TimeUnit.SECONDS.sleep(intervalTime);continue;}}return flag;}/**
* 具体业务
* @param retryNum
* @return
*/privatestaticbooleanexecute(int retryNum){
Random random=newRandom();int a= random.nextInt(10);boolean flag=true;try{if(a!=6){
flag=false;thrownewRuntimeException();}}catch(Exception e){//这里捕获异常只是为了能,返回flag的结果}return flag;}}
失败情况
成功情况
二.声明式使用Spring-Retry
2.1.如何使用
1.引入依赖
<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId></dependency><dependency><!--如果其他的依赖已经引入了,可以不加--><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency>
2.配置类
- 要使用
@EnabelRetry
开启重试才行,写在配置类或者启动类
上都是可以的
@Configuration@EnableRetrypublicclassRetryConfiguration{@Beanpublic PayServicepayService(){returnnewPayService();}}
3.实现类
- 服务类一般写主要逻辑,在需要重试的方法上面使用
@Retryable
//@Service@Slf4jpublicclassPayService{privatefinalint totalNum=100000;@Retryable(value={Exception.class}, maxAttempts=3, backoff=@Backoff(delay=2000, multiplier=1.5))publicintminGoodsNum(int num){
log.info("减库存开始=>"+ LocalTime.now());
log.info("库存=>"+ totalNum);if(num<=0){thrownewIllegalArgumentException("数量不对");}
log.info("减库存执行结束=>"+ LocalTime.now());return totalNum- num;}/**
* 使用@Recover注解,当重试次数达到设置的次数的时候,还是失败抛出异常,执行的回调函数。
*/@Recoverpublicintrecover(Exception e){
log.warn("减库存失败!!!"+ LocalTime.now());//记日志到数据库return totalNum;}}
测试代码
@RunWith(SpringRunner.class)@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)publicclassRetryTest{@Autowiredprivate PayService payService;/**
* 应该使用 org.junit.Test 才是正确的
*/@TestpublicvoidminGoodsNum(){
payService.minGoodsNum(-1);}}
结果
@Retryable
修饰的minGoodsNum()
方法,如果调用期间报了异常
,那么它将进行重试3次(默认三次),如果超过最大重试次数,则执行@Retryable修饰的recover()
方法,
@Retryable注解有各种配置,用于包含和排除异常类型、限制重试次数和回退策略。
2.2.常用注解
@EnableRetry:启用重试,proxyTargetClass属性为true时(默认false),使用CGLIB代理
@Retryable:标记当前方法会使用重试机制
- value:指定抛出那些异常才会触发重试(
可以配置多个异常类型
) 默认为空- include:
就是value
,默认为空
,当exclude也为空时,默认所有异常都可以触发重试- exclude:指定那些异常不触发重试(
可以配置多个异常类型
),默认为空- maxAttempts:最大重试次数,
默认3次
(包括第一次调用)- backoff:重试等待策略
默认使用@Backoff注解
@Backoff:重试回退策略(立即重试还是等待一会再重试)
- value: 重试的间隔时间默认为
1000L
,我们设置为2000L
- delay:重试的间隔时间,
就是value
- maxDelay:重试次数之间的最大时间间隔,
默认为0
,如果小于delay的设置,则默认为30000L
- multiplier:delay时间的间隔倍数,
默认为0,表示固定暂停1秒后进行重试
,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
.
- 不设置参数时,默认使用
FixedBackOffPolicy(固定时间等待策略),重试等待1000ms
只设置delay时
,使用FixedBackOffPolicy,重试等待指定的毫秒数- 当设置
delay和maxDealy
时,重试等待在这两个值之间均态分布 - 设置
delay,maxDealy和multiplier
时,使用ExponentialBackOffPolicy(倍数等待策略)
- 当设置
multiplier不等于0
时,同时也设置了random
时,使用ExponentialRandomBackOffPolicy(随机倍数等待策略)
@Recover标记方法为@Retryable失败时的“兜底”处理方法
- 传参与@Retryable的配置的value必须一样
- @Recover标记方法必须要
与@Retryable注解的方法“形参”保持一致
,第一入参为要重试的异常(一定要是@Retryable方法里抛出的异常或者异常父类)
,其他参数与@Retryable保持一致,返回值
也要一样,否则无法执行!
@CircuitBreaker:用于标记方法,实现熔断模式。
- include 指定处理的异常类。默认为空
- exclude指定不需要处理的异常。默认为空
- vaue指定要重试的异常。默认为空
- maxAttempts 最大重试次数。默认3次
- openTimeout 配置熔断器打开的超时时间,默认5s,当超过openTimeout之后熔断器电路变成半打开状态(只要有一次重试成功,则闭合电路)
- resetTimeout 配置熔断器重新闭合的超时时间,默认20s,超过这个时间断路器关闭
2.3.注意事项
使用了
@Retryable注解
的方法直接实例化
调用不会触发重试,要先将实现类实例化到Spring容器
中,然后通过注入
等方式使用Spring-Retry是通过捕获异常的方式来触发重试的,@Retryable标注方法产生的异常
不能使用try-catch捕获
,要在方法上抛出异常,不然不会触发重试
重试原则
- 查询可以进行重试
- 写操作要慎重,除非业务方支持重入
三.编程式使用Spring-Retry
3.1.核心类
- RetryOperations : 定义了“重试”的基本框架(模板),要求传入RetryCallback,可选传入RecoveryCallback;
- RetryCallback: 封装你需要重试的业务逻辑;
- RecoverCallback:封装在多次重试都失败后"兜底"的业务逻辑;
- RetryTemplate:
RetryOperations的具体实现
,组合了RetryListener[],BackOffPolicy,RetryPolicy。 - RetryContext:
重试下的上下文
,可用于在多次Retry或者Retry 和Recover之间传递参数或状态; - RetryPolicy :
重试的策略
,可以固定次数的重试,也可以是指定超时时间进行重试; - BackOffPolicy:
重试的等待策略
,在业务逻辑执行发生异常时。如果需要重试,我们可能需要等一段时间(可能服务器过于繁忙,如果一直不间隔重试可能拖垮服务器),当然这段时间可以是 0,也可以是固定的,可以是随机的 - RetryListener:典型的“监听者”,在重试的不同阶段通知“监听者”;
3.2.RetryOperations
RetryOperations是重试的顶级接口:
- RetryOperations: 统一定义了重试的API(
定义了实现“重试”的基本框架(模板)
) - RetryTemplate: 是
RetryOperations
的模板模式实现
,实现了重试和熔断etryTemplate将重试、提供健壮和不易出错的API供大家使用。
提供的API如下:
publicinterfaceRetryOperations{<T, EextendsThrowable> Texecute(RetryCallback<T, E> var1)throws E;<T, EextendsThrowable> Texecute(RetryCallback<T, E> var1, RecoveryCallback<T> var2)throws E;<T, EextendsThrowable> Texecute(RetryCallback<T, E> var1, RetryState var2)throws E, ExhaustedRetryException;<T, EextendsThrowable> Texecute(RetryCallback<T, E> var1, RecoveryCallback<T> var2, RetryState var3)throws E;}
- RetryCallback:
编写需要执行重试的业务逻辑
,定义好业务逻辑后,就是如何重试的问题了。 - RetryTemplate: 通过设置不同的重试策略来控制如何进行重试。
默认的重试策略是SimpleRetryPlicy(会重试3次)
第1次重试如果成功后面就不会继续重试了。如果3次都重试失败了流程结束或者返回兜底结果。而返回兜底结果需要配置
RecoveyCallBack
- RecoveyCallBack:从名字可以看出这是一个兜底回调接口,
也就是重试失败后执行的逻辑。
当重试超过最大重试时间或最大重试次数后可以调用
RecoveryCallback进行恢复
,比如返回假数据或托底数据。
3.3.RetryPolicy(重试策略)
RetryPolicy: 重试策略的顶级接口
publicinterfaceRetryPolicyextendsSerializable{booleancanRetry(RetryContext var1);
RetryContextopen(RetryContext var1);voidclose(RetryContext var1);voidregisterThrowable(RetryContext var1, Throwable var2);}
方法说明:
- canRetry:在
每次重试
的时候调用,是否可以继续重试的判断条件 - open:
重试开始前
调用,会创建一个重试上下文到RetryContext
,保存重试的堆栈等信息
- registerThrowable:
每次重试异常
时调用(有异常会继续重试)
以 SimpleRetryPolicy为例,当重试次数达到3(默认3次)停止重试,重试次数保存在重试上下文中
RetryPolicy提供了如下策略实现:
- NeverRetryPolicy:只允许
调用RetryCallback一次
,不允许重试 - AlwaysRetryPolicy:允许
无限重试
,直到成功,此方式逻辑不当会导致死循环
- SimpleRetryPolicy:
固定次数
重试策略,默认重试最大次数为3次
,RetryTemplate默认使用的策略 - TimeoutRetryPolicy:
超时时间
重试策略,默认超时时间为1秒
,在指定的超时时间内允许重试 - ExceptionClassifierRetryPolicy:设置
不同异常
的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试 - CircuitBreakerRetryPolicy:有
熔断功能
的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
- delegate:真正执行的重试策略,由
构造方法传入
,当重试失败时,则执行熔断策略,默认SimpleRetryPolicy策略
- openTimeout:openWindow,熔断器电路打开的超时时间,当超过openTimeout之后熔断器电路变成半打开状态(主要有一次重试成功,则闭合电路),
默认5000毫秒
- resetTimeout:timeout,重置熔断器重新闭合的超时时间。
默认20000毫秒
- delegate:真正执行的重试策略,由
- CompositeRetryPolicy:
组合重试
策略,有两种
组合方式乐观组合重试策略:指只要有一个策略允许重试即可以
悲观组合重试策略:指只要有一个策略不允许重试即可以但不管哪种组合方式,组合中的每一个策略都会执行
3.4.BackOffPolicy(重试回退(等待)策略)
回退策略: 指的是每次重试是立即重试还是等待一段时间后重试
。默认情况下是立即重试
,如果需要配置
等待一段时间后重试则需要指定回退策略
比如是网络错误,立即重试将导致立即失败,最好等待一小段时间后重试,还要防止很多服务同时重试导致DDos。
- BackOffPolicy是回退策略的顶级接口
BackOffPolicy 提供了如下策略实现
NoBackOffPolicy:
无等待
策略,每次重试时立即重试FixedBackOffPolicy:
固定时间
的等待策略需设置参数
sleeper和backOffPeriod
- sleeper指定等待策略,默认是Thread.sleep,即线程休眠
- backOffPeriod指定休眠时间,默认1000毫秒
UniformRandomBackOffPolicy:
随机时间
回退策略需设置
sleeper、minBackOffPeriod和maxBackOffPeriod
该策略在[minBackOffPeriod,maxBackOffPeriod
之间取一个随机休眠时间。- sleeper指定等待策略,默认是Thread.sleep,即线程休眠
- minBackOffPeriod 默认500毫秒
- maxBackOffPeriod 默认1500毫秒
ExponentialBackOffPolicy:
倍数
等待策略需设置参数
sleeper、initialInterval、maxInterval和multiplier
- sleeper指定等待策略,默认是Thread.sleep,即线程休眠
- initialInterval指定初始休眠时间,默认100毫秒
- maxInterval指定最大休眠时间,默认30秒
- multiplier指定乘数,即下一次休眠时间为
当前休眠时间*multiplier
,之前说过固定倍数可能会引起很多服务同时重试导致DDos,使用随机休眠时间来避免这种情况。
ExponentialRandomBackOffPolicy:
随机倍数
等待策略,引入随机乘数可以实现随机乘数回退
3.5.RetryTemplate主要流程实现源码
RetryTemplate类是对RetryOperations的具体实现
,组合了RetryListener[],BackOffPolicy,RetryPolicy
。
3.5.1.RetryTemplate.doExecute()方法
protected<T, EextendsThrowable> TdoExecute(RetryCallback<T, E> retryCallback,
RecoveryCallback<T> recoveryCallback, RetryState state)throws E, ExhaustedRetryException{//重试策略
RetryPolicy retryPolicy=this.retryPolicy;//退避策略
BackOffPolicy backOffPolicy=this.backOffPolicy;//重试上下文,当前重试次数等都记录在上下文中
RetryContext context=open(retryPolicy, state);try{//拦截器模式,执行RetryListener#openboolean running=doOpenInterceptors(retryCallback, context);//判断是否可以重试执行while(canRetry(retryPolicy, context)&&!context.isExhaustedOnly()){try{//执行RetryCallback回调return retryCallback.doWithRetry(context);}catch(Throwable e){//异常时,要进行下一次重试准备//遇到异常后,注册该异常的失败次数registerThrowable(retryPolicy, state, context, e);//执行RetryListener#onErrordoOnErrorInterceptors(retryCallback, context, e);//如果可以重试,执行退避算法,比如休眠一小段时间后再重试if(canRetry(retryPolicy, context)&&!context.isExhaustedOnly()){
backOffPolicy.backOff(backOffContext);}//在有状态重试时,如果是需要执行回滚操作的异常,则立即抛出异常//shouldRethrow方法只有一行代码: state != null && state.rollbackFor(context.getLastThrowable())if(shouldRethrow(retryPolicy, context, state)){throw RetryTemplate.<E>wrapIfNecessary(e);}}//如果是有状态重试,且有GLOBAL_STATE属性,则立即跳出重试终止;当抛出的异常是非需要执行回滚操作的异常时,//才会执行到此处,CircuitBreakerRetryPolicy会在此跳出循环;if(state!= null&& context.hasAttribute(GLOBAL_STATE)){break;}}//重试失败后,如果有RecoveryCallback,则执行此回调,否则抛出异常returnhandleRetryExhausted(recoveryCallback, context, state);}catch(Throwable e){throw RetryTemplate.<E>wrapIfNecessary(e);}finally{//清理环境close(retryPolicy, context, state, lastException== null|| exhausted);//执行RetryListener#close,比如统计重试信息doCloseInterceptors(retryCallback, context, lastException);}}
3.5.2.如何使用RetryTemplate
@TestpublicvoidtestSimple()throws Exception{@DataclassFoo{private String id;}
RetryTemplate template=newRetryTemplate();//超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
TimeoutRetryPolicy policy=newTimeoutRetryPolicy();
policy.setTimeout(3000L);
template.setRetryPolicy(policy);
Foo result= template.execute(//可能触发重试的业务逻辑newRetryCallback<Foo, Exception>(){@Overridepublic FoodoWithRetry(RetryContext context){try{
System.out.println("调用百度接口。。。。");
TimeUnit.MILLISECONDS.sleep(500);thrownewRuntimeException("调用百度接口超时");}catch(InterruptedException e){//e.printStackTrace();}returnnewFoo();}},//重试耗尽后的回调,配置了RecoveryCallback,超出重试次数后不会抛出异常,而是执行回调里的代码newRecoveryCallback<Foo>(){//可以没有RecoveryCallback,超出重试次数后依然会抛出异常@Overridepublic Foorecover(RetryContext context)throws Exception{
System.out.println("调用百度接口。。。recover");returnnewFoo();}});
System.out.println("result=>"+ result);}
我们模拟调用百度并将结果返回给用户,如果该调用失败,则重试该调用,直到达到超时为止。
如果不使用RecoveryCallback,当重试失败后,当重试失败后,抛出异常
如果使用RecoveryCallback,,当重试失败后,执行RecoveryCallback
3.6.状态重试 OR 无状态重试
3.6.1.无状态重试
无状态重试: 是在一个循环中执行完重试策略,即重试上下文保持在一个线程上下文中,在一次调用中进行完整的重试策略判断。
非常简单的情况,如远程调用某个查询方法
时是最常见的无状态重试。SimpleRetryPolicy就属于无状态重试
,因为重试是在一个循环
中完成的。
publicstaticvoidmain(String[] args){
RetryTemplate template=newRetryTemplate();//重试策略:次数重试策略
RetryPolicy retryPolicy=newSimpleRetryPolicy(3);
template.setRetryPolicy(retryPolicy);//退避策略:倍数回退策略
ExponentialBackOffPolicy backOffPolicy=newExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(100);
backOffPolicy.setMaxInterval(3000);
backOffPolicy.setMultiplier(2);
backOffPolicy.setSleeper(newThreadWaitSleeper());
template.setBackOffPolicy(backOffPolicy);
String resul= template.execute(newRetryCallback<String, RuntimeException>(){@Overridepublic StringdoWithRetry(RetryContext context)throws RuntimeException{
System.out.println(