目录
需求背景
解决思路
方法一
方法二
方法三
解决案例
1. pom引用
2. 举个栗子
3. 实战代码梳理
4. 结语
接到需求如下:超时处理机制,若调用xx风控中台服务后,在指定超时时间内,未收到应答,则平台需要再次调用xx风控中台服务,各风险侦测服务的具体超时时间,参见各接口规范balabalabala
上面需求明确指出,如接口调用超时,需要再次发起,即重试。
java的重试:
最原始的for或者while就可以。不用多说,最初级的攻城狮应该都能写出来的,对吧?
据了解,spring有注解,可通过spring-retry注解@Retryable来实现。再了解,发现这个注解的条件比较苛刻。需要在@Configuration注解的类中添加@EnableRetry,最后在想要重试的方法上添加@Retryable(Exception.class)由于retry用到了aspect增强,所有会有aspect的坑,就是方法内部调用,会使aspect增强失效,那么retry当然也会失效。也就说,方法体内调用是不行的,而且一个类中只能有一个@Retryable。方法中不支持throw异常信息等,不支持返回(说法不够严谨),不够灵活。
guava-retrying,是一个java解决重试的工具包。此方式更加灵活,便于提取公共方法类,低耦合,可以返回和支持方法中throw异常。方法写了,提供给新手使用不是更好吗?
guava-retrying项目地址https://github.com/rholder/guava-retrying。
com.github.rholder
guava-retrying
2.0.0
import com.github.rholder.retry.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @Author : whb
* @Description : guava重试demo
*/
@Slf4j
public class GuavaRetryDemo {
public static void main(String[] args) {
//定义请求实现
Callable callable = () -> {
// do something useful here
log.info("重试了一次");
throw new RuntimeException();
};
//定义重试机制
Retryer retryer = RetryerBuilder.newBuilder()
// retryIf 重试条件
.retryIfResult(Objects::isNull)
//设置异常重试源
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.retryIfResult(res -> res = false)
//设置等待间隔时间
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
//设置最大重试次数
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(callable);
} catch (RetryException | ExecutionException e) {
e.printStackTrace();
}
}
a. 提取重试策略类
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.common.base.Predicates;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.net.SocketTimeoutException;
import java.util.concurrent.TimeUnit;
/**
* @ClassName UpRiskClientRetryer
* 风控查询,超时后重试策略
* @Author wuhebin
* @Date 2022/4/26 9:16
* @Version 1.0
**/
@Slf4j
@Component
public class UpRiskClientRetryer {
//定义通用的重试器
private Retryer defaultRetryer;
//定义其他的重试器
private Retryer millRetryer;
/**
* @Author wuhebin
* 重试间隔时长20s
* @Date 2022/4/26 10:24
**/
@Bean(value = "defaultRetryer")
public Retryer defaultRetryer(){
log.info("===========init defaultRetryer==========");
defaultRetryer = RetryerBuilder.newBuilder()
//retryIf 重试条件
//retryIfException,抛出 runtime 异常、checked 异常时都会重试,但是抛出 error 不会重试。
//.retryIfException()
//retryIfRuntimeException 只会在抛 runtime 异常的时候才重试,checked 异常和error 都不重试。
//.retryIfRuntimeException()
//retryIfExceptionOfType 允许我们只在发生特定异常的时候才重试,比如NullPointerException 和 IllegalStateException 都属于 runtime 异常,也包括自定义的error。
.retryIfExceptionOfType(SocketTimeoutException.class)
// .retryIfException(Predicates.equalTo(new Exception()))
//retryIfResult 可以指定你的 Callable 方法在返回值的时候进行重试,如 返回false时重试
// .retryIfResult(Predicates.equalTo(false))
//等待策略:每次请求间隔1s
.withWaitStrategy(WaitStrategies.fixedWait(20, TimeUnit.SECONDS))
//停止策略 : 尝试请求3次
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
return defaultRetryer;
}
/**
* @Author wuhebin
* 重试间隔时长2s
* @Date 2022/4/26 10:24
**/
@Bean(value = "millRetryer")
public Retryer millRetryer(){
log.info("===========init millRetryer==========");
millRetryer = RetryerBuilder.newBuilder()
//retryIf 重试条件
.retryIfExceptionOfType(SocketTimeoutException.class)
//等待策略:每次请求间隔1s
.withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit.SECONDS))
//停止策略 : 尝试请求3次
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
return millRetryer;
}
}
b. 其他类调用
defaultRetryer.call(myServiceCallable)
c. 提取重试方法
@Autowired
private Retryer defaultRetryer;
@Autowired
private Retryer millRetryer;
/**
* @Author wuhebin
* 超时重试策略
* @Date 2022/4/26 10:09
* @Param
* @return
**/
private JSONObject doRetry(String url , int milliseconds,RiskQueryHeaderVo headerVo, JSONObject inMessage){
Callable myServiceCallable = new Callable() {
int times = 1;
@Override
public JSONObject call() throws Exception{
log.info("地址:"+url+",请求超时,重试了{}次", times);
return doRiskClientService(url, milliseconds, headerVo, inMessage);
}
};
try {
//黑白名单的重试间隔是2s,其他是20s
if(milliseconds==RiskConstant.R_CARD_BLACK_LIST_TIMEOUT){
return (JSONObject)millRetryer.call(myServiceCallable);
}
return (JSONObject)defaultRetryer.call(myServiceCallable);
} catch (ExecutionException e) {
e.printStackTrace();
return null;
} catch (RetryException e) {
e.printStackTrace();
return null;
}
}
/**
* @Author wuhebin
* 风控接口服务
* @Date 2022/4/20 9:51
* @Param url 地址
* @Param milliseconds 超时毫秒
* @Param headerVo 一些头部参数
* @Param inMessage 主体报文内容
* @return
**/
public JSONObject doRiskClientService(String url , int milliseconds,RiskQueryHeaderVo headerVo, JSONObject inMessage) throws Exception{
// throw new SocketTimeoutException();//测试超时重试
JSONObject respObj = JSONUtil.parseObj("{\"msg\":\"请求业务成功\",\"code\":\"RT1000\",\"subMsg\":\"\",\"subCode\":\"\",\"data\":{\"C401\":\"20\",\"C402\":\"5\",\"C403\":\"12\"}}");
.......HttpRequest.post......
return respObj;
}
d. 使用
public RiskQueryRespVo doQueryPersonVfyService(String reqId, JSONObject inMessage) {
.......
JSONObject respObj = doRetry(url,RiskConstant.R_URP_TIMEOUT,headerVo,inMessage);
......
}
guava-retrying也存在弊端,代码不够优雅,相比spring-retry注解方式,代码显得更多,但是个人认为方便提取公共类给小白使用的工具才是好的工具,不一定要优雅。