概述
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事务管理建议采用编程式进行细粒度事务管理。