Programmatic Transaction Management in Spring – Spring中的程序性事务管理

最后修改: 2019年 10月 25日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Spring’s @Transactional annotation provides a nice declarative API to mark transactional boundaries.

Spring的@Transactional注解提供了一个很好的声明性API来标记事务性边界。

Behind the scenes, an aspect takes care of creating and maintaining transactions as they are defined in each occurrence of the @Transactional annotation. This approach makes it easy to decouple our core business logic from cross-cutting concerns such as transaction management.

在幕后,一个方面负责创建和维护事务,因为它们是在@Transactional注解的每次出现中定义的。这种方法使我们能够轻松地将核心业务逻辑与交易管理等跨领域问题解耦。

In this tutorial, we’ll see that this isn’t always the best approach. We’ll explore what programmatic alternatives Spring provides, such as TransactionTemplate, and our reasons for using them.

在本教程中,我们将看到,这并不总是最好的方法。我们将探讨Spring提供了哪些程序化的替代方案,例如TransactionTemplate,以及我们使用它们的原因。

2. Trouble in Paradise

2.天堂里的麻烦

Let’s suppose we’re mixing two different types of I/O in a simple service:

让我们假设我们在一个简单的服务中混合了两种不同类型的I/O。

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

Here we have a few database calls alongside a possibly expensive REST API call. At first glance, it might make sense to make the whole method transactional since we may want to use one EntityManager to perform the whole operation atomically.

在这里,我们有几个数据库调用和一个可能昂贵的REST API调用。乍一看,使整个方法成为事务性的可能是有意义的,因为我们可能想使用一个EntityManager来原子化地执行整个操作。

However, if that external API takes longer than usual to respond for whatever reason, we may soon run out of database connections!

然而,如果该外部API由于某种原因需要比平时更长的时间来响应,我们可能很快就会耗尽数据库连接!”。

2.1. The Harsh Nature of Reality

2.1.现实的严酷性

Here’s what happens when we call the initialPayment method:

下面是我们调用initialPayment方法时的情况。

  1. The transactional aspect creates a new EntityManager and starts a new transaction, so it borrows one Connection from the connection pool.
  2. After the first database call, it calls the external API while keeping the borrowed Connection.
  3. Finally, it uses that Connection to perform the remaining database calls.

If the API call responds very slowly for a while, this method would hog the borrowed Connection while waiting for the response.

如果API调用的响应速度在一段时间内非常缓慢,这个方法将在等待响应的同时占用借用的Connection

Imagine that during this period we get a burst of calls to the initialPayment method. In that case, all Connections may wait for a response from the API call. That’s why we may run out of database connections — because of a slow back-end service!

想象一下,在此期间,我们得到了对initialPayment方法的一阵调用。在这种情况下,所有的连接可能会等待API调用的响应。这就是为什么我们可能会耗尽数据库连接–因为后端服务太慢!

Mixing the database I/O with other types of I/O in a transactional context isn’t a great idea. So, the first solution for these sorts of problems is to separate these types of I/O altogether. If for whatever reason we can’t separate them, we can still use Spring APIs to manage transactions manually.

在事务背景下将数据库 I/O 与其他类型的 I/O 混在一起并不是一个好主意。因此,这类问题的第一个解决方案是将这些类型的I/O完全分开。如果由于某种原因我们无法将它们分开,我们仍然可以使用Spring API来手动管理事务。

3. Using TransactionTemplate

3.使用TransactionTemplate

TransactionTemplate provides a set of callback-based APIs to manage transactions manually. In order to use it, we should first initialize it with a PlatformTransactionManager.

TransactionTemplate提供了一组基于回调的API,用于手动管理事务。为了使用它,我们应该首先用一个PlatformTransactionManager来初始化它。

We can set up this template using dependency injection:

我们可以使用依赖性注入来设置这个模板。

// test annotations
class ManualTransactionIntegrationTest {

    @Autowired
    private PlatformTransactionManager transactionManager;

    private TransactionTemplate transactionTemplate;

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

    // omitted
}

The PlatformTransactionManager helps the template to create, commit or roll back transactions.

PlatformTransactionManager 帮助模板创建、提交或回滚事务。

When using Spring Boot, an appropriate bean of type PlatformTransactionManager will be automatically registered, so we just need to simply inject it. Otherwise, we should manually register a PlatformTransactionManager bean.

当使用Spring Boot时,一个合适的PlatformTransactionManager类型的bean将被自动注册,所以我们只需要简单地注入它。否则,我们应该手动注册一个PlatformTransactionManagerbean。

3.1. Sample Domain Model

3.1.领域模型样本

From now on, for the sake of demonstration, we’re going to use a simplified payment domain model.

从现在开始,为了便于演示,我们将使用一个简化的支付领域模型。

In this simple domain, we have a Payment entity to encapsulate each payment’s details:

在这个简单的领域中,我们有一个支付实体来封装每个支付的细节。

@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
    }
}

Also, we’ll run all tests inside a test class, using the Testcontainers library to run a PostgreSQL instance before each test case:

另外,我们将在一个测试类里面运行所有的测试,使用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("baeldung")
                .withUsername("test")
                .withPassword("test");
        pg.setPortBindings(singletonList("54320:5432"));

        return pg;
    }
}

3.2. Transactions With Results

3.2.有结果的事务

The TransactionTemplate offers a method called execute, which can run any given block of code inside a transaction and then return some result:

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();
}

Here we’re persisting a new Payment instance into the database and then returning its auto-generated id.

在这里,我们将一个新的支付实例持久化到数据库,然后返回其自动生成的id。

Similar to the declarative approach, the template can guarantee atomicity for us.

与声明式方法类似,模板可以为我们保证原子性

If one of the operations inside a transaction fails to complete, it rolls back all of them:

如果一个事务内部的一个操作未能完成,它就会回滚所有的操作。

@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();
}

Since the second referenceNumber is a duplicate, the database rejects the second persist operation, causing the whole transaction to roll back. Therefore, the database does not contain any payments after the transaction.

由于第二个referenceNumber 是重复的,数据库拒绝第二个持久化操作,导致整个交易回滚。因此,数据库在交易后不包含任何付款。

It’s also possible to manually trigger a rollback by calling the setRollbackOnly() on TransactionStatus:

也可以通过调用setRollbackOnly()TransactionStatus手动触发回滚。

@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();
}

3.3. Transactions Without Results

3.3.没有结果的事务

If we don’t intend to return anything from the transaction, we can use the TransactionCallbackWithoutResult callback class:

如果我们不打算从交易中返回任何东西,我们可以使用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);
}

3.4. Custom Transaction Configurations

3.4.自定义事务配置

Up until now, we used the TransactionTemplate with its default configuration. Although this default is more than enough most of the time, it’s still possible to change configuration settings.

到目前为止,我们使用TransactionTemplate的默认配置。虽然这个默认值在大多数情况下是绰绰有余的,但仍有可能改变配置设置。

Let’s set the transaction isolation level:

我们来设置交易隔离级别

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

Similarly, we can change the transaction propagation behavior:

同样地,我们可以改变事务传播行为。

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

Or we can set a timeout, in seconds, for the transaction:

或者我们可以为交易设置一个超时时间,以秒为单位。

transactionTemplate.setTimeout(1000);

It’s even possible to benefit from optimizations for read-only transactions:

甚至有可能从只读事务的优化中获益。

transactionTemplate.setReadOnly(true);

Once we create a TransactionTemplate with a configuration, all transactions will use that configuration to execute. So, if we need multiple configurations, we should create multiple template instances.

一旦我们创建了一个带有配置的TransactionTemplate,所有事务将使用该配置来执行。因此,如果我们需要多个配置,我们应该创建多个模板实例。

4. Using PlatformTransactionManager

4.使用PlatformTransactionManager

In addition to the TransactionTemplate, we can use an even lower-level API such as PlatformTransactionManager to manage transactions manually. Quite interestingly, both @Transactional and TransactionTemplate use this API to manage their transactions internally.

除了TransactionTemplate,我们还可以使用更低级别的API,比如PlatformTransactionManager来手动管理交易。相当有趣的是,@TransactionalTransactionTemplate都使用这个API来内部管理他们的交易。

4.1. Configuring Transactions

4.1.配置事务

Before using this API, we should define how our transaction is going to look.

在使用这个API之前,我们应该定义我们的交易将是什么样子。

Let’s set a three-second timeout with the repeatable read transaction isolation level:

让我们用可重复的读事务隔离级别设置一个三秒钟的超时。

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

Transaction definitions are similar to TransactionTemplate configurations. However, we can use multiple definitions with just one PlatformTransactionManager.

事务定义与TransactionTemplate配置类似。然而,我们可以只用一个PlatformTransactionManager使用多个定义。

4.2. Maintaining Transactions

4.2.维护事务

After configuring our transaction, we can programmatically manage transactions:

在配置好我们的交易后,我们可以以编程方式管理交易。

@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);
}

5. Conclusion

5.总结

In this article, we first saw when we should choose programmatic transaction management over the declarative approach.

在这篇文章中,我们首先看到了什么时候我们应该选择程序化的事务管理而不是声明式的方法。

Then, by introducing two different APIs, we learned how to manually create, commit or roll back any given transaction.

然后,通过引入两个不同的API,我们学会了如何手动创建、提交或回滚任何特定的事务。

As usual, the sample code is available over on GitHub.

像往常一样,样本代码可在GitHub上获得。