使用场景
在实际工作中,重处理是一个非常常见的场景,比如:调用第三方接口或者使用mq时发送消息失败,调用远程服务失败,争抢锁失败,等等,这些错误可能是因为网络波动造成的,等待过后重处理就能成功.通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,并且不是很方便,要多写很多代码.然而spring-retry却可以通过注解,在不入侵原有业务逻辑代码的方式下,优雅的实现重处理功能.
思路
使用@Retryable和@Recover实现重处理,以及重处理失后的回调
实现步骤一:
在Springboot工程的pom文件中引入spring-retry
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
实现步骤二:
应用启动类开启retry-- @EnableRetry
实现步骤三:
在指定方法上标记@Retryable来开启重试
@Service
public class PayService {
private Logger logger = LoggerFactory.getLogger(getClass());
private final int totalNum = 100000;
@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 2000L, multiplier = 1.5))
public int minGoodsnum(int num) throws Exception {
logger.info("减库存开始" + LocalTime.now());
try {
int i = 1 / 0;
} catch (Exception e) {
logger.error("illegal");
}
if (num <= 0) {
throw new IllegalArgumentException("数量不对");
}
logger.info("减库存执行结束" + LocalTime.now());
return totalNum - num;
}
}
@Retryable
的参数说明:
- value:抛出指定异常才会重试
- include:和value一样,默认为空,当exclude也为空时,默认所以异常
- exclude:指定不处理的异常
- maxAttempts:最大重试次数,默认3次
- backoff:重试等待策略,默认使用
@Backoff
,@Backoff
的value默认为1000L,我们设置为2000L;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
实现步骤四:
写一个测试类进行验证:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootRetryApplicationTests {
@Autowired
private PayService payService;
@Test
public void payTest() throws Exception {
int store = payService.minGoodsnum(-1);
System.out.println("库存为:" + store);
}
}
运行测试类后的效果:
可以看到,三次之后抛出了IllegalArgumentException
异常。
当重试耗尽时,RetryOperations可以将控制传递给另一个回调,即RecoveryCallback。
Spring-Retry还提供了@Recover注解来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中)
此方法里的异常一定要是@Retryable方法里抛出的异常,否则不会调用这个方法。
@Recover
public int recover(Exception e) {
logger.warn("减库存失败!!!" + LocalTime.now());
return totalNum;
}
在Service->PayService 中,加上如上的方法之后,进行测试。
可以看到当三次重试执行完之后,会调用Recovery方法,也不会再次抛出异常。
注意事项:
网上有人说,重试机制,不能在接口实现类里面写。我还没有验证,但是一般的做法是把需要重试的方法和对应的重试方式写到单独的一个service,比如如下。
private OauthUserAttribute requestLoginApi(OauthFormLoginParam oauthFormLoginParam) {
// 为了防止 UPMS 接口抖动,这里做了 retry 机制
OauthUserAttribute oauthUserAttribute = retryService.getOauthUserAttributeBO(oauthFormLoginParam.getUsername(), oauthFormLoginParam.getPassword());
if (null == oauthUserAttribute || StringUtil.isBlank(oauthUserAttribute.getUserId())) {
log.error("调用 UPMS 接口返回错误信息,用户名:<{}>", oauthFormLoginParam.getUsername());
throw new OauthApiException("演示模式下,用户名:admin,密码:123456", ResponseProduceTypeEnum.HTML, GlobalVariable.DEFAULT_LOGIN_PAGE_PATH);
}
return oauthUserAttribute;
}
单独的重试service:
@Service
@Slf4j
public class RetryService {
@Autowired
private OauthThirdPartyApiService oauthThirdPartyApiService;
//=====================================调用验证用户名密码的 retry 逻辑 start=====================================
@Retryable(value = {Exception.class}, maxAttempts = 2, backoff = @Backoff(delay = 2000L, multiplier = 1))
public OauthUserAttribute getOauthUserAttributeBO(String username, String password) {
return oauthThirdPartyApiService.getOauthUserAttributeDTO(username, password);
}
@Recover
public OauthUserAttribute getOauthUserAttributeBORecover(Exception e) {
log.error("多次重试调用验证用户名密码接口失败=<{}>", e.getMessage());
return new OauthUserAttribute();
}
//=====================================调用验证用户名密码的 end=====================================
}