在后台对接第三方接口时,由于网络波动等原因,经常会出现请求超时的情况,但是有些接口不能因为一次超时就判定为失败,通常都会有重试,比如连续请求三次都失败,才认为请求失败。
方案一:在代码中进行逻辑判断
以前在处理这类问题的时候,我们大多采用的是这种方案。即设置一个重试次数,然后每次调用后判断是否调用成功,如果调用失败且没有超过重试次数,则重试调用。如下
@Test
void invoke() {
int failCount = 0;
while (failCount < 3) {
if (doInvoke()) {
break;
}
failCount++;
}
// 连续三次失败,记录失败日志
// logService.saveLog(...)
}
boolean doInvoke() {
// 模拟远程调用
boolean b = new Random().nextBoolean();
log.info("doInvoke result : {}", b);
return b;
}
方案二:框架自带重试机制
在微服务架构中,几乎所有框架都支持超时重试,如dubbo、springcloud的feign都有超时重试机制。作为服务的调用方,我们可以不考虑接口的幂等性,默认为提供方已经做好了幂等处理,这时候我们只需要设置一下超时时间和重试次数即可。如
@DubboReference(retries = 2)
private OrderInfoService orderInfoService;
方案三:使用spring retry
spring retry原先是springbatch中的一个模块,后来被独立出来,使用非常简单,也是本文主要介绍的用法。
需要添加如下maven依赖,然后在启动类或配置类上添加@EnableRetry
注解
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
只需要重点关注其中的两个注解@Retryable
和@Recover
,前者指定重试次数和重试生效条件以及重试时间间隔等,后者一般作为失败后的日志记录或失败补偿。
使用示例如下:
@Slf4j
@Service
public class PayService {
long start = 0;
@Retryable(recover = "recoverTest",
value = IllegalArgumentException.class,
maxAttempts = 10,
backoff = @Backoff(value = 1000L, maxDelay = 5000L, multiplier = 1.5))
public boolean pay() {
if (start == 0) {
start = System.currentTimeMillis();
}
// 记录前后间隔时间
log.info("pay....{}", System.currentTimeMillis() - start);
start = System.currentTimeMillis();
throw new IllegalArgumentException("问题不大...");
}
@Recover
public boolean recoverTest(IllegalArgumentException e) {
log.info("recover...{}", e.getMessage());
return false;
}
}
其中,@Retryable
的recover
指定了所有重试均失败后的回调方法,如果@Retryable
标注方法调用成功了,则不会调用@Recover
注解标注的方法,与原方法的返回类型一样,@Recover
注解标注的方法入参与@Retryable
注解标注方法中的value
或include
属性指定的异常类型对应,maxAttempts
指定最大重试次数,默认为3,backoff
指定重试时间间隔等,如上述示例中,重试间隔时间每次都是上一次的1.5倍,且最大不超过5秒。
执行后日志如下
2022-02-16 10:51:03.045 INFO 9224 --- [ main] com.example.service.PayService : pay....0
2022-02-16 10:51:04.046 INFO 9224 --- [ main] com.example.service.PayService : pay....1000
2022-02-16 10:51:05.546 INFO 9224 --- [ main] com.example.service.PayService : pay....1500
2022-02-16 10:51:07.796 INFO 9224 --- [ main] com.example.service.PayService : pay....2250
2022-02-16 10:51:11.171 INFO 9224 --- [ main] com.example.service.PayService : pay....3375
2022-02-16 10:51:16.171 INFO 9224 --- [ main] com.example.service.PayService : pay....5000
2022-02-16 10:51:21.171 INFO 9224 --- [ main] com.example.service.PayService : pay....5000
2022-02-16 10:51:26.171 INFO 9224 --- [ main] com.example.service.PayService : pay....5000
2022-02-16 10:51:31.171 INFO 9224 --- [ main] com.example.service.PayService : pay....5000
2022-02-16 10:51:36.171 INFO 9224 --- [ main] com.example.service.PayService : pay....5000
2022-02-16 10:51:36.171 INFO 9224 --- [ main] com.example.service.PayService : recover...问题不大...
@Retryable
注解中还有其他属性,如支持表达式的exceptionExpression
,例如在该注解中添加exceptionExpression = "message.contains('出大问题啦...')"
,则将不会进行异常重试,因为异常错误信息不匹配。至于其他的属性,就不再举例了,很少用到,上面这些个属性几乎足够用了。