Spring-Retry(重試機制)

​ 在實際工作中,重處理是一個非常常見的場景,比如:發送消息失敗、調用遠程服務失敗、爭搶鎖失敗。
這些錯誤可能是因爲網絡波動造成的,等待過後重處理就能成功。通常來說,會用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回調方法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章