知識庫 / Spring / Spring Boot RSS 訂閱

處理 Spring 中的意外回滾異常

Spring Boot,Spring Data
HongKong
9
10:36 AM · Dec 06 ,2025

1. 概述

當處理嵌套事務時,可能會出現一些特定問題,這些問題與嵌套本身有關。 尤其常見的問題通常會導致 <em >UnexpectedRollbackException</em >》。 <strong >這發生在事務中的一個操作失敗,並且我們嘗試在同一事務中執行另一個數據庫操作時</strong >。 在這種情況下,我們通常會看到一個相當令人困惑的錯誤消息:Transaction rolled back because it has been marked as rollback-only>`。

在本教程中,我們將理解為什麼即使捕獲了異常,<em >UnexpectedRollbackException</em >> 仍然會發生。 此外,我們還將通過創建單獨的事務邊界來修復或規避它。 尤其是,我們可以通過使用不同的傳播級別或使用TransactionTemplate> 編程方式管理事務來實現。

2. 瞭解問題

對於這裏提供的示例,我們假設我們想要構建一個類似於 Baeldung 的博客網站後端

重點在於一個使用場景,即通過將文章保存到數據庫來發布文章。無論保存操作是否成功,我們還希望在審計表中記錄相應的條目。

2.1. 模擬問題

讓我們從以下代碼開始,該代碼將 文章實例保存到數據庫並記錄 審計記錄,該代碼使用了 博客類:

博客類用於保存 文章實例到數據庫,並記錄關於結果的 審計記錄。

@Component
class Blog {

    private final ArticleRepo articleRepo;
    private final AuditRepo auditRepo;

    // constructor

    @Transactional
    public Optional<Long> publishArticle(Article article) {
        try {
            article = articleRepo.save(article);
            auditRepo.save(
              new Audit("SAVE_ARTICLE", "SUCCESS", "saved: " + article.getTitle()));
            return Optional.of(article.getId());

        } catch (Exception e) {
            String errMsg = "failed to save: %s, err: %s".formatted(article.getTitle(), e.getMessage());
            auditRepo.save(
              new Audit("SAVE_ARTICLE", "FAILURE", errMsg));
            return Optional.empty();
        }
    }

}

乍一看,這似乎沒問題——如果保存文章失敗,我們捕獲異常並插入審計條目以指示失敗。但是,嘗試發佈一個無效的 Article 會拋出 UnexpectedRollbackException,錯誤消息為 Transaction rolled back because it has been marked as rollback-only

2.2. 測試

讓我們編寫一個集成測試,以確認這種行為。例如,我們可以嘗試以一個 null 的作者發佈一篇文章:

@SpringBootTest
class ArticleServiceIntegrationTest {

    @Autowired
    private Blog articleService;

    @Autowired
    private ArticleRepo articleRepo;

    @Autowired
    private AuditRepo auditRepo;

    @BeforeEach
    void afterEach() {
        articleRepo.deleteAll();
        auditRepo.deleteAll();
    }

    @Test
    void whenPublishingAnInvalidArticle_thenThrowsUnexpectedRollbackException() {
        assertThatThrownBy(
             () -> articleService.publishArticle(new Article("Test Article", null)))
          .isInstanceOf(UnexpectedRollbackException.class)
          .hasMessageContaining("marked as rollback-only");

        assertThat(auditRepo.findAll())
          .isEmpty();
    }

}

由於文章由於缺少作者而無效,因此會拋出異常並回滾交易。 因此,不僅文章表單的插入操作失敗,而且審計記錄也未保存

2.3. 只回滾事務

原因在於 Spring 如何管理事務。當我們使用 @Transactional 時,Spring 會為該方法啓動一個事務。如果 articleRepo.save() 拋出異常,Spring 會將當前事務標記為 只回滾

當我們隨後嘗試在 try-catch 塊中持久化 Audit 條目,它仍然在同一事務中運行。方法結束時,Spring 嘗試提交,檢測到該事務已被標記為回滾,並拋出 UnexpectedRollbackException 異常

3. 使用嵌套事務 via AOP

為了解決當前問題,我們可能需要在單獨的事務中執行 審計 插入操作。 通過使用 Spring 中的 @Transactional 註解並設置不同的傳播設置,可以實現這一點。

3.1. 傳播類型

具體來説,我們希望 審計操作在單獨的事務中執行。由於 Spring 使用 AOP 代理來處理 @Transactional,因此該解決方案需要一個單獨的代理。 實現所需結果的最簡單方法是提取一個專門用於與 審計數據交互的新類:

@Service
class AuditService {

    private final AuditRepo auditRepo;

    // constructor

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveAudit(String action, String status, String message) {
        auditRepo.save(new Audit(action, status, message));
    }

}

如我們所見,saveAudit() 也標記為 @Transactional。 默認情況下,如果它從另一個事務方法中調用,它將參與到現有的事務中。 但是,我們使用 Propagation.REQUIRES_NEW 覆蓋了默認行為,因此 Spring 始終為保存 Audit 實體創建新的事務

現在,我們可以更新主服務以調用此方法。 讓我們創建一個新的方法,publishArticle_v2(),以便我們可以輕鬆地比較這兩種方法:

@Transactional
public Optional<Long> publishArticle_v2(Article article) {
    try {
        article = articleRepo.save(article);
        auditService.saveAudit("SAVE_ARTICLE", "SUCCESS", "saved: " + article.getTitle());
        return Optional.of(article.getId());

    } catch (Exception e) {
        auditService.saveAudit("SAVE_ARTICLE", "FAILURE", "failed to save: " + article.getTitle());
        return Optional.empty();
    }
}

因此,我們可以使用 AOP 和 @Transactional 註解來定義事務範圍,但它需要添加額外的抽象層,以便 Spring 可以創建必要的 AOP 代理。 在本示例中,我們不得不引入 AuditService,其唯一目的是委託給 AuditRepo 並覆蓋事務傳播級別以啓動一個新的事務。

3.2. 嵌套事務

我們所做的是,實際上創建了一個新的嵌套事務,同時保持了初始事務的完整性。這意味着 審計 操作將在其自身的獨立事務中運行,因此即使主事務最終失敗,也能成功提交:

下面我們編寫一個簡單的測試,以驗證這種新方法仍然由於無效的文章 Article 拋出異常,但成功地將記錄插入到 audit 表中:

@Test
void whenPublishingAnInvalidArticle_thenSavesFailureToAudit() {
    assertThatThrownBy(
         () -> articleService.publishArticle_v2(new Article("Test Article", null)))
      .isInstanceOf(Exception.class);

    assertThat(auditRepo.findAll())
      .extracting("description")
      .containsExactly("failed to save: Test Article");
}

正如預期的那樣,主交易可以自由回滾並拋出 Java 異常,同時失敗記錄仍然存在於 審計 表中。

4. 使用順序事務,通過 <em TransactionTemplate</em>> 實現

與其使用嵌套事務,我們可以選擇在啓動第二個事務之前,確保首先提交或回滾第一個事務

由於在 Spring AOP 中精確定義事務範圍可能比較複雜,因此我們現在使用 <em TransactionTemplate</em>> Bean。

允許我們通過編程方式定義事務邊界,從而對事務的開始和結束時間具有更精細的控制。例如,我們可以使用它來為保存 `` 啓動一個事務,並在發生失敗時,確保在將其記錄到審計表中之前關閉它。 這樣,JPA 就可以為審計操作啓動一個新的事務:

@Component
class Blog {

    private final ArticleRepo articleRepo;
    private final AuditRepo auditRepo;
    private final TransactionTemplate transactionTemplate;

    // constructor

    public Optional publishArticle_v3(final Article article) {
        try {
            Article savedArticle = transactionTemplate.execute(txStatus -> {
                Article saved = articleRepo.save(article);
                auditRepo.save(
                  new Audit("SAVE_ARTICLE", "SUCCESS", "saved: " + article.getTitle()));
                return saved;
            }); // <-- transaction ends here

            return Optional.of(savedArticle.getId());

        } catch (Exception e) {
            auditRepo.save(
              new Audit("SAVE_ARTICLE", "FAILURE", "failed to save: " + article.getTitle()));
            return Optional.empty();
        }
    }
}

如果測試這個解決方案,我們可能會期望它能夠優雅地處理錯誤並返回一個空Optional。 Needless to say,它也應該在審計表中記錄失敗:

@Test
void whenPublishingAnInvalidArticle_thenRecoverFromError_andSavesFailureToAudit() {
    Optional<Long> id = articleService.publishArticle_v3(new Article("Test Article", null));

    assertThat(id).isEmpty();

    assertThat(auditRepo.findAll())
      .extracting("description")
      .containsExactly("failed to save: Test Article");
}

此時,我們確保交易的安全處理。

5. 結論

在本文中,我們探討了 Spring 中嵌套事務的常見問題,這可能導致 UnexpectedRollbackException。 尤其,我們研究了僅僅在事務方法中捕獲異常是否足夠,以及 Spring 如何將事務標記為 僅回滾

隨後,我們介紹了兩個實用解決方案:

  • 使用具有不同傳播的單獨事務邊界
  • 使用 TransactionTemplate 編程管理事務

正如一貫的,本文中的代碼可在 GitHub 上找到:https://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-boot-persistence-5

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.