在實際工作中,重處理是一個非常常見的場景,比如:發送消息失敗、調用遠程服務失敗、爭搶鎖失敗。
這些錯誤可能是因爲網絡波動造成的,等待過後重處理就能成功。通常來說,會用try/catch,while循環之類的語法來進行重處理,但是這樣的做法缺乏統一性,並且不是很方便,要多寫很多代碼。然而spring-retry卻可以通過註解,在不入侵原有業務邏輯代碼的方式下,優雅的實現重處理功能。
業務背景:地推系統中,原有註冊用戶與地推用戶綁定關係是在前端註冊成功之後使用MQ消息,在MQ消息中進行消費,綁定註冊用戶與地推用戶關係,MQ消費失敗之後會使用消息重試,進行最多5次消費,因此次和商城對接地推服務商品,綁定關係邏輯需要在地推系統登錄日誌接口中記錄,無法適用MQ消費重試,故需要在service方法中進行調用重試,保證綁定關係的成功率。
一、@Retryable是什麼?
spring系列的spring-retry是另一個實用程序模塊,可以幫助我們以標準方式處理任何特定操作的重試。在spring-retry中,所有配置都是基於簡單註釋的。
二、使用步驟
1.POM依賴
基於AOP實現,因此還需引入aop相關的依賴
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
2.啓用@Retryable
@SpringBootApplication
@Import(GlobalExceptionTranslator.class)
@EnableFeignClients(basePackages = {"com.jzt.jk"})
@EnableTransactionManagement
@EnableRocketMq
@EnableFaServer
@EnableSoaServiceClients(baseScanPackage = "com.jzt.jk")
@EnableRetry
public class DistributionApplication {
public static void main(String[] args) {
SpringApplication.run(DistributionApplication.class, args);
}
}
3.在方法上添加@Retryable
1.原調用方引用RetryableCustomerUserService
@Slf4j
@Service
public class CustomerVisitLogService extends ServiceImpl<CustomerVisitLogDao, CustomerVisitLog> {
@Resource
RetryableCustomerUserService retryableCustomerUserService;
/**
* 新增用戶訪問日誌
*
* @param request
* @return
*/
public void addCustomerVisitLog(CustomerVisitLogCreateReq request) {
CustomerVisitLog customerVisitLog = modelMapper.map(request, CustomerVisitLog.class);
//獲取地推信息並組裝
getDistributionInfo(customerVisitLog);
customerVisitLog.setEventName(CustomerVisitLogEventEnum.getCustomerVisitLogEventEnum(request.getEvent()).getName());
customerVisitLog.setAppName(AppIdEnum.getAppIdEnum(request.getAppId()).getDesc());
customerVisitLog.setSceneName(getSceneName(request.getAppId(), request.getScene()));
customerVisitLog.setCreateTime(new Date());
customerVisitLog.setRecommendNo(request.getRecommendNo());
//有地推人員就把推薦標的信息放入日誌
if (request.getDistributorId() != null && (AppIdEnum.USER_WX_MINI_APP.getName().equals(request.getAppId())
|| AppIdEnum.USER_WX_OFFICIAL_ACCOUNT.getName().equals(request.getAppId()) || AppIdEnum.USER_WX_OFFICIAL_XFWY_ACCOUNT.getName()
.equals(request.getAppId()))) {
QrcodeExtraByIdResp resp = null;
//兼容歷史小程序,查詢最早可用的二維碼
if (StringUtils.isNotBlank(request.getRecommendNo())) {
resp = userQrcodeService.getQrcodeExtraByRecommendNo(request.getRecommendNo());
} else {
log.warn("有地推id沒有推薦編碼,取最早創建的可用的小程序碼,request={}", JSON.toJSONString(request));
resp = userQrcodeService.getQrcodeExtraById(request.getDistributorId());
}
log.info("resp={}", JSON.toJSONString(resp));
if (resp != null) {
customerVisitLog.setRecommendNo(resp.getRecommendNo());
if (resp.getUserQrcodeExtDto() != null) {
customerVisitLog.setTeamDiseaseCenterId(resp.getUserQrcodeExtDto().getTeamDiseaseCenterId());
customerVisitLog.setTeamDiseaseCenterName(resp.getUserQrcodeExtDto().getTeamDiseaseCenterName());
customerVisitLog.setDiseaseTeamId(resp.getUserQrcodeExtDto().getTeamId());
customerVisitLog.setDiseaseTeamName(resp.getUserQrcodeExtDto().getTeamName());
}
}
}
if (!save(customerVisitLog)) {
log.error("新增用戶訪問日誌失敗.request={}", request);
throw new BusinessException("新增用戶訪問日誌失敗");
}
//註冊事件添加綁定關係
if (CustomerVisitLogEventEnum.REGISTER.getEvent().equals(request.getEvent()) && request.getDistributorId() != null) {
DistributionThreadPoolExecutor.submit(() -> {
log.info("appId:{},distributorId:{},customerUserId:{},訪問日誌記錄註冊事件,調用註冊用戶綁定地推用戶關係開始", request.getAppId(), request.getDistributorId(),
request.getCustomerUserId());
DistributionRegisterMsg registerMsg = new DistributionRegisterMsg();
registerMsg.setAppId(request.getAppId());
registerMsg.setRegisterTime(customerVisitLog.getCreateTime().getTime());
registerMsg.setDistributorId(request.getDistributorId() + "");
registerMsg.setCustomerUserId(request.getCustomerUserId());
retryableCustomerUserService.retryableRecommendRegistry(registerMsg);
});
}
}
}
2.創建service實現類並添加@Retryable,因爲@Retryable是基於AOP切面實現,故不能在當前類中調用執行,不然不能生效
@Slf4j
@Service
public class RetryableCustomerUserService {
@Resource
private CustomerUserService customerUserService;
/**
* value:拋出指定異常纔會重試
* include:和value一樣,默認爲空,當exclude也爲空時,默認所有異常
* exclude:指定不處理的異常
* maxAttempts:最大重試次數,默認3次
* backoff:重試等待策略,
* 默認使用@Backoff,@Backoff的value默認爲1000L,我們設置爲2000; 以毫秒爲單位的延遲(默認 1000)
* multiplier(指定延遲倍數)默認爲0,表示固定暫停1秒後進行重試,如果把multiplier設置爲1.5,則第一次重試爲2秒,第二次爲3秒,第三次爲4.5秒。
* @param code
* @return
* @throws Exception
*/
@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 1.5))
public void retryableRecommendRegistry(DistributionRegisterMsg msgEntity) {
log.info("調用可重試的推薦註冊方法:{}", JSON.toJSONString(msgEntity));
customerUserService.recommendRegistry(msgEntity);
}
/**
* Spring-Retry還提供了@Recover註解,用於@Retryable重試失敗後處理方法。
* 如果不需要回調方法,可以直接不寫回調方法,那麼實現的效果是,重試次數完了後,如果還是沒成功沒符合業務判斷,就拋出異常。
* 可以看到傳參裏面寫的是 Exception e,這個是作爲回調的接頭暗號(重試次數用完了,還是失敗,我們拋出這個Exception e通知觸發這個回調方法)。
* 注意事項:
* 方法的返回值必須與@Retryable方法一致
* 方法的第一個參數,必須是Throwable類型的,建議是與@Retryable配置的異常一致,其他的參數,需要哪個參數,寫進去就可以了(@Recover方法中有的)
* 該回調方法與重試方法寫在同一個實現類裏面
*
* 由於是基於AOP實現,所以不支持類裏自調用方法
* 如果重試失敗需要給@Recover註解的方法做後續處理,那這個重試的方法不能有返回值,只能是void
* 方法內不能使用try catch,只能往外拋異常
* @Recover註解來開啓重試失敗後調用的方法(注意,需跟重處理方法在同一個類中),此註解註釋的方法參數一定要是@Retryable拋出的異常,否則無法識別,可以在該方法中進行日誌處理。
* @param e
* @param msgEntity 與被重試參數列表一致
* @return
*/
@Recover
public void recover(Exception e, DistributionRegisterMsg msgEntity) {
log.info("調用可重試的推薦註冊方法異常回調:{}", JSON.toJSONString(msgEntity));
}
}
來簡單解釋一下註解中幾個參數的含義:
- value:拋出指定異常纔會重試
- include:和value一樣,默認爲空,當exclude也爲空時,默認所有異常
- exclude:指定不處理的異常
- maxAttempts:最大重試次數,默認3次
- backoff:重試等待策略,默認使用@Backoff,@Backoff的value默認爲1000(單位毫秒),我們設置爲2000;multiplier(指定延遲倍數)默認爲0,表示固定暫停1秒後進行重試,如果把multiplier設置爲1.5,則第一次重試爲2秒,第二次爲3秒,第三次爲4.5秒。
當重試耗盡時還是失敗,會出現什麼情況呢?
當重試耗盡時,RetryOperations可以將控制傳遞給另一個回調,即RecoveryCallback。Spring-Retry還提供了@Recover註解,用於@Retryable重試失敗後處理方法。如果不需要回調方法,可以直接不寫回調方法,那麼實現的效果是,重試次數完了後,如果還是沒成功沒符合業務判斷,就拋出異常。
- 重試方法必須要使用
@Recover
註解; - 返回值必須和被重試的函數返回值一致;
- 參數中除了第一個是觸發的異常外,後面的參數需要和被重試函數的參數列表一致;
@Recover
public void recover(Exception e, DistributionRegisterMsg msgEntity) {
log.info("調用可重試的推薦註冊方法異常回調:{}", JSON.toJSONString(msgEntity));
}
查看測試結果:
可以看到總共執行了三次,最後失敗調用recover回調方法