天天看点

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