如何使用SpringBoot的重試功能模組? - Gavin

banq發表於2021-11-21

重試功能是 Spring Batch 模組的一部分。從 2.2.0 開始,此功能從 Spring Batch 中提取出來並作為一個單獨的模組進行維護。要在 Spring 應用程式中啟用此功能,請將此依賴項包含到您的 maven pom.xml 中。

<dependency> 
  <groupId>org.springframework.retry</groupId> 
  <artifactId>spring-retry</artifactId> 
  <version>1.3.1.RELEASE</version> 
</dependency>

該庫不帶有自動配置,因此@EnableRetry應將註解新增到 SpringBoot App 或帶有@Configuration註解的類中,以啟用重試功能。

 

宣告式方法——構建重試邏輯的快速簡便方法

下面的示例程式碼指定了以下設定:

  • 最多 3 次重試
  • 每次重試與乘法器之間的範圍為 0.5 秒 - 3 秒的隨機間隔
  • 僅針對 RuntimeException 觸發重試,這意味著系統會立即針對其他異常(例如客戶端錯誤或驗證拒絕)丟擲異常。

@Service
public class RetryableCustomerSrvClient {
    @Autowired
    private CustomerSrvClient customerSrvClient;

    @Retryable(value = RuntimeException.class, maxAttempts = 4, backoff = @Backoff(delay = 500L, maxDelay = 3000L, multiplier = 2, random = true))
    public Optional<Customer> getCustomer(Long id) {
        return customerSrvClient.getCustomer(id);
    }
}

註釋無需編碼即可神奇地工作。

起先,系統邏輯呼叫 Customer API 客戶端上的一個方法來檢索客戶資料,而無需重試註釋。

應用註解

@Retryable
後,Spring 框架在執行時引入了一個代理,該代理通過重試邏輯處理客戶 API 客戶端上的方法呼叫。由於代理是在系統啟動時建立的,它對產品報價邏輯是完全透明的,因此不需要更改程式碼。

引數也可以由系統屬性指定。但是,如果您想要更動態的東西,這種方法可能不適合您。例如,註解不支援基於產品型別的不同重試設定,除非為每個產品型別指定了單獨的方法呼叫。

在這種情況下,使用命令式風格可以通過動態重試設定來實現這樣的系統需求。

 

命令的方法——支援動態重試策略

Spring 框架為命令式方法提供了一個實用程式類RetryTemplate。這是一種“侵入性”方法,涉及更改程式程式碼,以便系統邏輯使用RetryTemplate來檢索客戶資料。下圖顯示它類似於宣告式方法中的代理,但是它不是在執行時建立的。

下面的示例程式碼根據產品型別應用不同的重試策略。使用RetryTemplate顯然是一個優勢,因為它允許靈活地自定義重試策略作為系統邏輯的一部分。

此示例程式碼使用宣告式方法實現了與上一個類似的重試邏輯。它展示了根據產品程式碼確定最大嘗試次數的邏輯的靈活性。

private Optional<Product> retrieveProduct(String productCode) {

    int maxAttempts = productCode.startsWith(TRAVEL_INSURANCE_PREFIX)? 5 : 2;

    RetryTemplate retryTemplate = RetryTemplate.builder()
            .maxAttempts(maxAttempts)
            .retryOn(RuntimeException.class)
            .exponentialBackoff(300L, 2, 5000L, true)
            .build();

    return retryTemplate.execute(arg -> productSrvClient.getProduct(productCode));
}

 

重試資料插入/更新

重試不僅適用於資料查詢。它可以應用於其他過程,例如資料插入/更新的 I/O 操作。想象一下,一個消耗資源並涉及多個步驟的系統程式,您絕對不希望該程式僅僅因為它在程式結束時未能將結果儲存到資料庫中而崩潰。系統偶爾會在 I/O 操作上遇到錯誤並不少見,例如,由於併發訪問可能導致記錄鎖定。當系統再次重試時,I/O 操作將成功完成。

請記住,該操作應該是冪等的。換句話說,多次執行操作時,結果應該是不變的。例如,如果多次執行該操作,則只會在資料庫中插入一條新記錄,而不是重複記錄。

如果記錄的主鍵是由 MySQL 根據自增代理 id 生成的,那麼每次應用程式邏輯儲存一個引用記錄時都會建立一個新記錄。因此,將通過重試建立重複的記錄。

因此,應用程式程式碼應該準備主鍵,而不是依賴 MySQL 中的序列號,以實現冪等的資料插入。報價的樣本資料模型表明報價程式碼為記錄ID。

@Data
@Builder
@Entity
@Table(name = "quotation")
public class Quotation {
    @Id
    private String quotationCode;

    private Double amount;

    @JsonFormat (shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime expiryTime;

    private String productCode;

    private Long customerId;
}

並且應用邏輯為引用程式碼分配一個唯一的 UUID

Quotation generateAndSaveQuotation(QuotationReq request, Customer customer, Product product) {

    // generate quotation price
    Double quotationAmount = generateQuotation(request, customer, product);

    // Construct quotation
    Quotation quotation = Quotation.builder()
            .quotationCode(UUID.randomUUID().toString())
            .customerId(customer.getId())
            .expiryTime(now.plusMinutes(quotationExpiryTime))
            .productCode(request.getProductCode())
            .amount(quotationAmount)
            .build();

    // Save into data store
    return saveQuotation(quotation);
}

Quotation saveQuotation(Quotation quotation) {
    RetryTemplate retryTemplate = RetryTemplate.builder()
            .maxAttempts(3)
            .fixedBackoff(1000L)
            .build();
    return retryTemplate.execute(arg -> quotationRepo.save(quotation));
}

 

為重試邏輯構建自動化測試

對重試邏輯的驗證並不容易。大多數情況下,很難模擬外部服務和資料庫中的錯誤。另一種方法是使用 Mockito 模擬錯誤情況。這是用於驗證save()引用儲存庫中方法的示例單元測試程式碼。

  • 場景 1 - 所有嘗試都失敗

模擬重試失敗的場景很簡單,你可以將模擬 bean 配置為在涉及目標方法時始終丟擲異常。

@Test
void givenAllRetryOnQuotationSaveExhausted_whenRequestForQuotation_thenThrowException() throws IOException {
    
  // GIVEN 
  when(quotationRepo.save(any(Quotation.class)))
    .thenThrow(new RuntimeException("Mock Exception"));
    
  // other mock setup
  // ...
  
  // WHEN
  Quotation quotation = quotationService.generateQuotation(req);

  // THEN
  // verify exception
  RuntimeException exception = assertThrows(RuntimeException.class, () -> quotationService.generateQuotation(req));
}

  • 場景 2 — 前 2 次嘗試失敗,第三次嘗試成功

更復雜的情況是前 2 次嘗試失敗,然後第 3 次成功。此示例程式碼save()在前 2 次呼叫引用儲存庫方法時模擬異常錯誤,並在第 3 次嘗試時返回引用物件。Mockito 是一個方便的工具來模擬連續的函式呼叫。您可以簡單地連結模擬設定方法 -thenThrow()並按thenAnswer()順序連結。

@Test
void givenRetryOnQuotationSaveSuccess_whenRequestForQuotation_thenSuccess() throws IOException, RecordNotFoundException, QuotationCriteriaNotFulfilledException {
    
  // GIVEN 
  when(quotationRepo.save(any(Quotation.class)))
    .thenThrow(new RuntimeException("Mock Exception 1"))
    .thenThrow(new RuntimeException("Mock Exception 2"))
    .thenAnswer(invocation -> (Quotation) invocation.getArgument(0));
    
  // other mock setup
  // ...
  
  // WHEN
  Quotation quotation = quotationService.generateQuotation(req);

  // THEN
  // verify quotation
  // ....
}

請參閱此 GitHub 儲存庫以獲取包含重試邏輯和自動化測試用例的完整應用程式程式碼

相關文章