一、背景介绍
前天是来到公司实习的第五天,之前几天分给我的任务一直都是熟悉项目,阅读项目代码梳理其中某些接口或枚举类之间的关系,并书写文档。直到前天下午组长把我叫来说出了这样一个需求,在我们项目中有一个认证身份证号的service接口,该接口是调用第三方公司的接口,但是第三方公司的接口并不稳定,每天大约有1.5w个返回错误code值20001,但是在我们这个接口,将这个返回视为认证成功,这样以来就会造成用户在下一步进行操作的时候遇到问题。但是因为对方的接口在大部分情况下仍然是能返回正确的值,所以当对方接口返回20001的时候,我们可以尝试重试继续请求,看这样做是否能降低每天该接口的错误率。
二、解决方法
1.简单的try-catch-redo模式
这里我们列举一段代码来看一下这种方式
public void commonRetry(Map dataMap) throws InterruptedException {
Map paramMap = Maps.newHashMap();
paramMap.put("tableName", "creativeTable");
paramMap.put("ds", "20160220");
paramMap.put("dataMap", dataMap);
boolean result = false;
try {
result = uploadToOdps(paramMap);
if (!result) {
Thread.sleep(1000);
uploadToOdps(paramMap); //一次重试
}
} catch (Exception e) {
Thread.sleep(1000);
uploadToOdps(paramMap);//一次重试
}
}
2.try-catch-redo-retry strategy策略重试模式
public void commonRetry(Map dataMap) throws InterruptedException {
Map paramMap = Maps.newHashMap();
paramMap.put("tableName", "creativeTable");
paramMap.put("ds", "20160220");
paramMap.put("dataMap", dataMap);
boolean result = false;
try {
result = uploadToOdps(paramMap);
if (!result) {
reuploadToOdps(paramMap,1000L,10);//延迟多次重试
}
} catch (Exception e) {
reuploadToOdps(paramMap,1000L,10);//延迟多次重试
}
}
方案二不过在方案一的基础上加上了延迟时间重试策略,并没有太大改动
我们可以看到这样无论是方案一还是方案二的确能够解决我们刚才的问题,但是这样做的后果是正常的代码逻辑和重试逻辑强耦合,对正常逻辑预期结果被动重试触发,对于重试根源往往由于逻辑复杂被淹没,可能导致后续运维对于重试逻辑要解决什么问题产生不一致理解。重试正确性难保证而且不利于运维,原因是重试设计依赖正常逻辑异常或重试根源的臆测。
2.spring-retry框架模式(
关于spring-retry基本概念和通过重写类方式的开发,这里不再重复造轮子,直接去看这篇博客介绍的已经很全面
https://blog.csdn.net/u011116672/article/details/77823867)
第一步,在pom.xml文件夹下添加如下依赖
org.springframework.retry
spring-retry
org.aspectj
aspectjweaver
第二步,在springBoot项目启动主类上添加@EnableRetry表示开启重试机制,也可以使用@EnableRetry(proxyTargetClass=false)表示使用cglib
@SpringBootApplication
@EnableRetry
public class SpringbootRetryApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootRetryApplication.class, args);
}
}
第三步、在需要重试的方法上加上@Retryable注解
@Service
public class IdCardVerifyRetryService {
private static final Logger logger = LoggerFactory.getLogger(IdCardVerifyRetryService.class);
@Autowired
private VerifyClient verifyClient;
@Retryable(maxAttempts = 3, backoff = @Backoff(value = 100L), value = RetryException.class)
public VerifyIdcardVO retryable(VerifyUserInfoParam verifyUserInfoParam) throws Exception {
logger.error(verifyUserInfoParam.toString());
logger.error("in retryable function.");
VerifyIdcardVO verifyIdcardVO = verifyClient.verify(verifyUserInfoParam);
verifyIdcardVO.setCode(IdCardVerifyResultEnum.SYSTEM_ERROR);
if (verifyIdcardVO.getCode().equals(IdCardVerifyResultEnum.SYSTEM_ERROR)) {
logger.error("verify idcard error,starting retry.");
throw new RetryException("执行verify方法返回IdCardVerifyResultEnum.SYSTEM_ERROR");
}
return verifyIdcardVO;
}
}
使用Junit进行单元测试,可看出结果如下:
三、注解讲解和注意事项
基本注解:
@Retryable注解
被注解的方法发生异常时会重试
@Backoff注解
@Recover注解 (我这里没有使用到@Recover注解,原因见下注意事项)
@CircuitBreaker(没有使用断路器):用于方法,实现熔断模式。
include 指定处理的异常类。默认为空
exclude指定不需要处理的异常。默认为空
vaue指定要重试的异常。默认为空
maxAttempts 最大重试次数。默认3次
openTimeout 配置熔断器打开的超时时间,默认5s,当超过openTimeout之后熔断器电路变成半打开状态(只要有一次重试成功,则闭合电路)
resetTimeout 配置熔断器重新闭合的超时时间,默认20s,超过这个时间断路器关闭
当重试到达指定次数时,被注解的方法将被回调,可以在该方法中进行日志处理。需要注意的是发生的异常和入参类型一致时才会回调。
注意事项:
1、使用了@Retryable的方法不能在本类被调用,不然重试机制不会生效。也就是要标记为@Service,然后在其它类使用@Autowired注入或者@Bean去实例才能生效。
2、要触发@Recover方法,那么在@Retryable方法上不能有返回值,只能是void才能生效。
3、使用了@Retryable的方法里面不能使用try...catch包裹,要在发放上抛出异常,不然不会触发。
4、在重试期间这个方法是同步的,如果使用类似Spring Cloud这种框架的熔断机制时,可以结合重试机制来重试后返回结果。
5、Spring Retry不只能注入方式去实现,还可以通过API的方式实现,类似熔断处理的机制就基于API方式实现会比较宽松。
(尤其是第二条,@Retryable的方法不能在本类那样直接调用,由于retry用到了aspect增强,所有会有aspect的坑,就是方法内部调用,会使aspect增强失效,那么retry当然也会失效)