Spring @Retryable 與 @Transactional
1. 概述
可靠的重試機制的關鍵要求是確保每次重試都在獨立的資料庫事務中運作。如果所有嘗試都共用同一個事務,則最終可能會導致整個操作回溯-尤其是在早期嘗試將交易標記為僅用於回溯的情況下。
在本教程中,我們將探討重試事務方法的不同策略。
具體來說,我們將探討兩種避免此問題的方法,它們透過確保每次重試都創建一個新的事務來實現。首先,我們將使用面向切面編程 (AOP) 以及諸如@Transactional和@Retryable之類的註解。之後,我們將研究使用 Spring 的TransactionTemplate和RetryTemplate程式替代方案。
2. @Transactional和@Retryable
對於程式碼範例,我們假設這是一個類似 Baeldung 的部落格平台的後端服務。
讓我們專注於一個簡單的操作序列,例如檢索草稿文章、更新文章並保存更改——所有這些操作都在單一資料庫事務中完成:
@Component
class Blog {
private final ArticleRepository articles;
private final ArticleSlugGenerator slugGenerator;
// constructor
@Transactional
public Article publishArticle(Long draftId) {
Article article = articles.findById(draftId)
.orElseThrow();
article.setStatus(Article.Status.PUBLISHED);
article.setSlug(slugGenerator.randomSlug());
return articles.save(article);
}
}
由於網路問題或競態條件等情況,上述邏輯可能會失效,導致整個更新遺失。提高可靠性的簡單方法是增加一些額外的重試次數。
與其從頭開始建立自訂重試機制,我們可以使用spring-retry 。因此,讓我們將依賴項新增到pom.xml檔案中:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
現在,我們可以直接使用@Retryable註解來標記publishArticle()方法。預設情況下,此註解會使函數額外重試兩次,但我們可以使用maxAttempts屬性來覆寫此行為:
@Transactional
@Retryable(maxAttempts = 5)
public Article publishArticle(Long draftId) {
// ...
}
如前所述,我們需要確保重試機制在事務機制之前被呼叫。為了確保這一點,我們重寫了 Spring Retry 的order ,並將其設定為LOWEST_PRECEDENCE 。具體來說,我們直接在@EnableRetry註解中執行此操作:
@EnableRetry(order = Ordered.LOWEST_PRECEDENCE)
@SpringBootApplication
public class RetryableTransactionalApplication {
public static void main(String[] args) {
SpringApplication.run(RetryableTransactionalApplication.class, args);
}
}
因此, @Transactional和@Retryable的結合為我們提供了一種聲明式但功能強大的方式,可以為資料庫操作增加彈性。
3. TransactionTemplate和RetryTemplate
如果依賴切面呼叫順序有風險,我們可以使用程式設計方式。我們可以不使用面向切面程式設計 (AOP),而是使用TransactionTemplate和RetryTemplate API。這些模板使我們能夠完全控制事務邊界和重試邏輯,從而使執行行為更加明確。
此外,我們可以直接注入 bean,也可以使用建構器模式來自訂其行為:
@Component
class Blog {
private final ArticleRepository articles;
private final ArticleSlugGenerator slugGenerator;
private final TransactionTemplate transactionTemplate;
private final RetryTemplate retryTemplate = new RetryTemplateBuilder()
.maxAttempts(5)
.fixedBackoff(Duration.ofMillis(100))
.build();
// constructor
// ...
}
它們的工作原理相當簡單:
-
TransactionTemplate會封裝一個函數,並且每次都在一個新的事務中執行它。這確保每次嘗試都在一個全新的事務中運行,因此一次嘗試的失敗不會影響下一次嘗試。 -
RetryTemplate封裝了一個函數,並根據配置的重試策略執行該函數。在本例中,函數最多重試五次(5),每次重試之間固定間隔100毫秒。如果某次重試失敗,模板會自動重試,再次呼叫函數。
透過將它們結合起來,我們可以確保每次重試都在其自身的事務中運行,並且重試遵循所需的退避策略,所有這些都無需依賴 AOP 代理的順序:
public Article publishArticle_v2(Long draftId) {
return retryTemplate.execute(retryCtx ->
transactionTemplate.execute(txCtx -> {
Article article = articles.findById(draftId)
.orElseThrow();
article.setStatus(Article.Status.PUBLISHED);
article.setSlug(slugGenerator.randomSlug());
return articles.save(article);
})
);
}
這樣,我們就可以在不增加不必要複雜性的情況下提高系統的穩健性。
4. 結論
在這篇短文中,我們探討了在處理資料庫事務時實現重試策略的不同方法。關鍵在於,每次重試都應該在獨立的資料庫事務中運行,以確保資料一致性並避免不必要的回溯。
為了進行演示,我們使用了實際範例,並利用@Transactional和@Retryable註解來更好地理解 AOP 方法。最後,我們探討了一種程式設計方式,其中TransactionTemplate和RetryTemplate允許我們明確地定義交易程式碼區塊和可重試程式碼區塊的作用域和生命週期,從而完全控制重試和交易。
與往常一樣,本文中的程式碼可在 GitHub 上找到。