概述
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方法時發生的情況:
- Spring背景建立一個新的EntityManager并啟動一個新事務,是以從資料庫連接配接池借用一個Connection。
- 在第一次資料庫調用之後,調用外部API,同時保留借用的資料庫連接配接。
- 最後,使用該資料庫連接配接執行剩餘的資料庫調用。
如果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事務管理建議采用程式設計式進行細粒度事務管理。