使用場景
在實際工作中,重處理是一個非常常見的場景,比如:調用第三方接口或者使用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=====================================
}