天天看點

Spring Data之程式設計式事務管理TransactionTemplate

Spring Data之程式設計式事務管理TransactionTemplate

概述

Spring的@Transactional注解提供了一個很好的聲明性API來标記事務邊界。這種方法可以很容易地将我們的核心業務邏輯與交叉關注點(如事務管理)分離開來。

但是通過注解方式并不總是最好的方法。本文将探讨Spring提供的程式設計式替代方案,如TransactionTemplate,以及使用的原因。

問題點

假設在一個簡單的服務中混合了兩種不同類型的I/O:

@Transactional
public void initialPayment(PaymentRequest request) {
    savePaymentRequest(request); // DB
    callThePaymentProviderApi(request); // API
    updatePaymentState(request); // DB
    saveHistoryForAuditing(request); // DB
}           

上述場景有幾個資料庫調用以及可能昂貴的REST API調用。乍一看,整個方法具有事務性可能是有意義的,因為我們可能希望使用一個EntityManager以原子方式執行整個操作。

然而,如果外部API出于任何原因需要比通常更長的時間來響應,那可能很快就會耗盡資料庫連接配接!

  • 問題點分析

調用initialPayment方法時發生的情況:

  1. Spring背景建立一個新的EntityManager并啟動一個新事務,是以從資料庫連接配接池借用一個Connection。
  2. 在第一次資料庫調用之後,調用外部API,同時保留借用的資料庫連接配接。
  3. 最後,使用該資料庫連接配接執行剩餘的資料庫調用。

如果API調用在一段時間内響應非常緩慢,則此方法将在等待響應時占用借用的資料庫連接配接。

想象一下,在此期間,我們收到了對initialPayment方法的一系列調用。在這種情況下,所有連接配接都可能阻塞在等待API調用的響應。這就是為什麼可能會耗盡資料庫連接配接:因為後端服務速度慢!

在事務上下文中混合資料庫I/O和其他類型的I/O不是一個好主意。是以,解決這類問題的第一個方案是将這些類型的I/O完全分開。如果出于任何原因不能将它們分開,可以使用程式設計式事務管理API,比如TransactionTemplate手動管理事務。

使用TransactionTemplate

TransactionTemplate提供了一組基于回調API來手動管理事務。首先使用PlatformTransactionManager進行初始化。

// test annotations
class ManualTransactionIntegrationTest {

    @Autowired
    private PlatformTransactionManager transactionManager;

    private TransactionTemplate transactionTemplate;

    @BeforeEach
    void setUp() {
        transactionTemplate = new TransactionTemplate(transactionManager);
    }

    // omitted
}           

PlatformTransactionManager用于模闆建立、送出或復原事務。

當使用Spring Boot時,PlatformTransactionManager類型的bean将自動注冊,是以隻需要簡單地注入即可。否則,需要手動注冊PlatformTransactionManager bean。

  • Entity模型

示範例子使用簡化的支付域模型。

@Entity
public class Payment {

    @Id
    @GeneratedValue
    private Long id;

    private Long amount;

    @Column(unique = true)
    private String referenceNumber;

    @Enumerated(EnumType.STRING)
    private State state;

    // getters and setters

    public enum State {
        STARTED, FAILED, SUCCESSFUL
    }
}           

在測試類中運作的單元測試,使用Testcontainers庫在每個測試用例之前運作PostgreSQL執行個體:

@DataJpaTest
@Testcontainers
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = NONE)
@Transactional(propagation = NOT_SUPPORTED) // we're going to handle transactions manually
public class ManualTransactionIntegrationTest {

    @Autowired 
    private PlatformTransactionManager transactionManager;

    @Autowired 
    private EntityManager entityManager;

    @Container
    private static PostgreSQLContainer<?> pg = initPostgres();

    private TransactionTemplate transactionTemplate;

    @BeforeEach
    public void setUp() {
        transactionTemplate = new TransactionTemplate(transactionManager);
    }

    // tests

    private static PostgreSQLContainer<?> initPostgres() {
        PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:11.1")
                .withDatabaseName("testDb")
                .withUsername("test")
                .withPassword("test");
        pg.setPortBindings(singletonList("54320:5432"));

        return pg;
    }
}           
  • 執行事務

TransactionTemplate提供了一個execute的方法,可以在事務中運作任何給定的代碼塊,然後傳回一些結果:

@Test
void givenAPayment_WhenNotDuplicate_ThenShouldCommit() {
    Long id = transactionTemplate.execute(status -> {
        Payment payment = new Payment();
        payment.setAmount(1000L);
        payment.setReferenceNumber("Ref-1");
        payment.setState(Payment.State.SUCCESSFUL);

        entityManager.persist(payment);

        return payment.getId();
    });

    Payment payment = entityManager.find(Payment.class, id);
    assertThat(payment).isNotNull();
}           

将一個新的Payment執行個體持久化到資料庫中,然後傳回其自動生成的id。

與聲明式方法類似,事務模闆可以保證原子性。

如果事務中的一個操作未能完成,将復原所有操作:

@Test
void givenTwoPayments_WhenRefIsDuplicate_ThenShouldRollback() {
    try {
        transactionTemplate.execute(status -> {
            Payment first = new Payment();
            first.setAmount(1000L);
            first.setReferenceNumber("Ref-1");
            first.setState(Payment.State.SUCCESSFUL);

            Payment second = new Payment();
            second.setAmount(2000L);
            second.setReferenceNumber("Ref-1"); // same reference number
            second.setState(Payment.State.SUCCESSFUL);

            entityManager.persist(first); // ok
            entityManager.persist(second); // fails

            return "Ref-1";
        });
    } catch (Exception ignored) {}

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty();
}           

由于第二個referenceNumber是重複的,資料庫拒絕第二個持久化操作,導緻整個事務復原。是以,資料庫不包含交易後的任何付款。

也可以通過對TransactionStatus調用setRollbackOnly()手動觸發復原:

@Test
void givenAPayment_WhenMarkAsRollback_ThenShouldRollback() {
    transactionTemplate.execute(status -> {
        Payment payment = new Payment();
        payment.setAmount(1000L);
        payment.setReferenceNumber("Ref-1");
        payment.setState(Payment.State.SUCCESSFUL);

        entityManager.persist(payment);
        status.setRollbackOnly();

        return payment.getId();
    });

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty();
}           
  • 無結果傳回事務操作

如果事務中無需傳回任何内容,可以使用TransactionCallbackWithoutResult回調類:

@Test
void givenAPayment_WhenNotExpectingAnyResult_ThenShouldCommit() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            Payment payment = new Payment();
            payment.setReferenceNumber("Ref-1");
            payment.setState(Payment.State.SUCCESSFUL);

            entityManager.persist(payment);
        }
    });

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1);
}           
  • 自定義事務配置

上述用例都是使用具有預設配置的TransactionTemplate。預設值在大多數情況下已經足夠了,但仍然可以更改配置設定。

設定事務隔離級别:

transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);           

更改事務傳播行為:

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);           

可以為事務設定逾時(以秒為機關):

transactionTemplate.setTimeout(1000);           

甚至可以從隻讀事務的優化中獲益:

transactionTemplate.setReadOnly(true);           

一旦我們使用配置建立了TransactionTemplate,所有事務都将使用該配置來執行。是以,如果需要多個配置,應該建立多個模闆執行個體。

使用PlatformTransactionManager

除了TransactionTemplate之外,還可以使用更低級别的API(如PlatformTransactionManager)手動管理事務。其實@Transactional和TransactionTemplate都使用此API在内部管理其事務。

  • 配置事務

使用可重複讀取事務隔離級别設定三秒逾時:

DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
definition.setTimeout(3);           

事務定義類似于TransactionTemplate配置。但是,我們可以在一個PlatformTransactionManager中使用多個定義。

  • 管理事務

配置事務後,我們可以通過程式設計方式管理事務:

@Test
void givenAPayment_WhenUsingTxManager_ThenShouldCommit() {
 
    // transaction definition

    TransactionStatus status = transactionManager.getTransaction(definition);
    try {
        Payment payment = new Payment();
        payment.setReferenceNumber("Ref-1");
        payment.setState(Payment.State.SUCCESSFUL);

        entityManager.persist(payment);
        transactionManager.commit(status);
    } catch (Exception ex) {
        transactionManager.rollback(status);
    }

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1);
}           

結論

選擇程式設計式事務管理還是聲明式,取決于實際業務場景。存在多種不同類型I/O事務管理建議采用程式設計式進行細粒度事務管理。