Using transactions in Quarkus

quarkus-narayana-jta`扩展提供了一个事务管理器,它可以协调和将事务曝光给你的应用,如链接中所述: Jakarta Transactions规范(以前称为 Java Transaction API (JTA))。 在讨论 Quarkus 事务时,本指南引用 Jakarta Transactions 事务样式,并且只使用术语 _transaction_来解决它们。 此外,Quarkus 不支持分布式事务。这意味着 Java Transaction Service (JTS)、REST-AT、WS-Atomic Transaction 等传播事务上下文的模型不受 `narayana-jta 扩展支持。

Setting it up

无需担心在大多数情况下进行设置,而需要此扩展的扩展会将其简单地作为一个依赖项。例如,Hibernate ORM 将包括事务管理器,并将其正确设置。

例如,如果您未直接使用 Hibernate ORM,则可能需要显式将其作为一个依赖项添加。将以下内容添加到您的构建文件:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-narayana-jta</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-narayana-jta")

Starting and stopping transactions: defining your boundaries

您可以用 @Transactional 声明式定义您的事务边界,也可以用 QuarkusTransaction 以编程方式定义。此外,还可以直接使用 JTA UserTransaction API,不过这不如 QuarkusTransaction 用户友好。

Declarative approach

定义事务边界的简便方法是在入口方法上使用 @Transactional 注解 (jakarta.transaction.Transactional)。

@ApplicationScoped
public class SantaClausService {

    @Inject ChildDAO childDAO;
    @Inject SantaClausDAO santaDAO;

    @Transactional (1)
    public void getAGiftFromSanta(Child child, String giftDescription) {
        // some transaction work
        Gift gift = childDAO.addToGiftList(child, giftDescription);
        if (gift == null) {
            throw new OMGGiftNotRecognizedException(); (2)
        }
        else {
            santaDAO.addToSantaTodoList(gift);
        }
    }
}
1 此注解定义了您的事务边界,并将这个调用封装在一个事务中。
2 跨越事务边界的 RuntimeException 将回滚事务。

@Transactional 可用于控制方法级或类级的任何 CDI Bean 上的事务边界,确保事务化每个方法。其中包括 REST 端点。

您可以使用 @Transactional 的参数控制是否以及如何启动事务:

  • @Transactional(REQUIRED) (默认):未启动时启动一个事务,否则保留现有事务。

  • @Transactional(REQUIRES_NEW):未启动时启动一个事务;已启动一个现有事务时,挂起该事务并为该方法的边界启动一个新事务。

  • @Transactional(MANDATORY):未启动事务时,失败;否则在现有事务中工作。

  • @Transactional(SUPPORTS):如果已启动一个事务,则加入该事务;否则不使用事务工作。

  • @Transactional(NOT_SUPPORTED):如果已启动一个事务,则挂起该事务并在方法的边界内不使用事务来工作;否则不使用事务来工作。

  • @Transactional(NEVER):如果已启动一个事务,则引发异常;否则不使用事务来工作。

REQUIREDNOT_SUPPORTED 可能用处最大。通过此方法您可以确定某个方法在事务内还是在事务外运行。务必查看 JavaDoc,了解确切语义。

正如预期的那样,事务上下文将传播到 @Transactional 方法中嵌套的所有调用(在此示例中为 childDAO.addToGiftList()`和 `santaDAO.addToSantaTodoList())。将提交事务,除非运行时异常跨越方法边界。您可以使用 @Transactional(dontRollbackOn=SomeException.class) (或 rollbackOn)覆盖是否由异常强制回滚。

此外,还可以通过编程方式要求将事务标记为需回滚。为此注入一个 TransactionManager

@ApplicationScoped
public class SantaClausService {

    @Inject TransactionManager tm; (1)
    @Inject ChildDAO childDAO;
    @Inject SantaClausDAO santaDAO;

    @Transactional
    public void getAGiftFromSanta(Child child, String giftDescription) {
        // some transaction work
        Gift gift = childDAO.addToGiftList(child, giftDescription);
        if (gift == null) {
            tm.setRollbackOnly(); (2)
        }
        else {
            santaDAO.addToSantaTodoList(gift);
        }
    }
}
1 注入 TransactionManager 以便能够激活 setRollbackOnly 语义。
2 以编程方式决定是否回滚事务。

Transaction configuration

可以使用 @TransactionConfiguration 注释配合在条目方法或类级别设置的标准 @Transactional 注释高级配置事务。

@TransactionConfiguration 注释允许设置超时属性(以秒为单位),适用于在带注释的方法中创建的事务。

此注释只能放在描述事务的顶级方法上。在事务已开始后注释的嵌套方法将引发异常。

如果在类上定义,则等同于在所有使用 @Transactional 标记的类方法上进行定义。方法上定义的配置优先于类上定义的配置。

Reactive extensions

如果用 @Transactional 注释的方法返回响应值,例如:

  • CompletionStage (from the JDK)

  • Publisher (from Reactive-Streams)

  • 可以使用响应类型转换器转换成以上两种类型的任何类型

那么其行为略有不同,因为只有当返回的响应值结束时,事务才会结束。事实上,返回的响应值将被侦听,如果它以异常结束,则将标记事务以回滚,并只在响应值结束时提交或回滚。

这允许你的响应方法异步地继续对事务执行操作直到工作真正完成,而不仅仅是等到响应方法返回时。

如果你需要在响应管道中传播事务上下文,请参见 Context Propagation guide

Programmatic approach

你可以使用 QuarkusTransaction 上的静态方法来定义事务边界。这提供了两种不同的选项,一种是函数式方法,允许你在事务作用域内运行 lambda 表达式,另一种是使用明确的 begincommitrollback 方法。

import io.quarkus.narayana.jta.QuarkusTransaction;
import io.quarkus.narayana.jta.RunOptions;

public class TransactionExample {

    public void beginExample() {
        QuarkusTransaction.begin();
        //do work
        QuarkusTransaction.commit();

        QuarkusTransaction.begin(QuarkusTransaction.beginOptions()
                .timeout(10));
        //do work
        QuarkusTransaction.rollback();
    }

    public void runnerExample() {
        QuarkusTransaction.requiringNew().run(() -> {
            //do work
        });
        QuarkusTransaction.joiningExisting().run(() -> {
            //do work
        });

        int result = QuarkusTransaction.requiringNew()
                .timeout(10)
                .exceptionHandler((throwable) -> {
                    if (throwable instanceof SomeException) {
                        return RunOptions.ExceptionResult.COMMIT;
                    }
                    return TransactionExceptionResult.ROLLBACK;
                })
                .call(() -> {
                    //do work
                    return 0;
                });
    }
}

上面的示例展示了此 API 的几种不同使用方法。

第一个方法只是调用 begin,执行一些操作并提交它。创建的事务绑定到 CDI 请求作用域,因此如果在请求作用域销毁时事务仍然处于活动状态,那么它将自动回滚。这避免了显式捕获异常和调用 rollback 的需要,并且充当防止无意事务泄漏的安全措施,然而它确实意味着这只能在请求作用域活动时使用。方法中的第二个示例调用带有超时选项的 begin,然后回滚事务。

第二个方法展示了使用带有 QuarkusTransaction.runner(…​) 的 lambda 作用域事务;第一个示例只是在新的事务中运行 Runnable,第二个示例也这样做不过加入现有的事务(如果有),第三个示例使用一些特定选项调用 Callable。尤其是 exceptionHandler 方法可以用来控制是否在发生异常时回滚事务。

支持以下语义:

QuarkusTransaction.disallowingExisting()/DISALLOW_EXISTING

If a transaction is already associated with the current thread a QuarkusTransactionException will be thrown, otherwise a new transaction is started, and follows all the normal lifecycle rules.

QuarkusTransaction.joiningExisting()/JOIN_EXISTING

If no transaction is active then a new transaction will be started, and committed when the method ends. If an exception is thrown the exception handler registered by #exceptionHandler(Function) will be called to decide if the TX should be committed or rolled back. If an existing transaction is active then the method is run in the context of the existing transaction. If an exception is thrown the exception handler will be called, however a result of ExceptionResult#ROLLBACK will result in the TX marked as rollback only, while a result of ExceptionResult#COMMIT will result in no action being taken.

QuarkusTransaction.requiringNew()/REQUIRE_NEW

If an existing transaction is already associated with the current thread then the transaction is suspended, then a new transaction is started which follows all the normal lifecycle rules, and when it’s complete the original transaction is resumed. Otherwise, a new transaction is started, and follows all the normal lifecycle rules.

QuarkusTransaction.suspendingExisting()/SUSPEND_EXISTING

If no transaction is active then these semantics are basically a no-op. If a transaction is active then it is suspended, and resumed after the task is run. The exception handler will never be consulted when these semantics are in use, specifying both an exception handler and these semantics are considered an error. These semantics allows for code to easily be run outside the scope of a transaction.

Legacy API approach

一种不太容易的方法是注入 UserTransaction 并使用各种事务分隔方法。

@ApplicationScoped
public class SantaClausService {

    @Inject ChildDAO childDAO;
    @Inject SantaClausDAO santaDAO;
    @Inject UserTransaction transaction;

    public void getAGiftFromSanta(Child child, String giftDescription) {
        // some transaction work
        try {
            transaction.begin();
            Gift gift = childDAO.addToGiftList(child, giftDescription);
            santaDAO.addToSantaTodoList(gift);
            transaction.commit();
        }
        catch(SomeException e) {
            // do something on Tx failure
            transaction.rollback();
        }
    }
}

你不能在已经由 @Transactional 调用启动了事务的方法中使用 UserTransaction

Configuring the transaction timeout

你可以通过 quarkus.transaction-manager.default-transaction-timeout 属性(指定为持续时间)配置默认事务超时,该超时将适用于事务管理器管理的所有事务。

要编写持续时间值,需使用标准格式 java.time.Duration.更多信息,请参阅 Duration#parse() javadoc. 你还可以使用简化格式,从数字开始:

  • 如果该值为数字,则表示时间(以秒计)。

  • 如果该值为数字,后跟 ms,则表示时间(以毫秒计)。

在其他情况下,简化格式会转换为 java.time.Duration 格式进行解析:

  • 如果该值为数字,后跟 h, ms, 则前缀为 PT.

  • 如果该值为数字,后跟 d ,则前缀为 P.

默认值为 60 秒。

Configuring transaction node name identifier

作为底层事务管理器,Narayana 有一个唯一节点标识符的概念。如果您考虑使用涉及多个资源的 XA 事务,这一点非常重要。

节点名称标识符在事务标识中发挥至关重要的作用。创建事务时,节点名称标识符将转换为事务 ID。基于节点名称标识符,事务管理器能够识别在数据库或 JMS 代理中创建的 XA 事务对等项。事务管理器可以利用该标识符在恢复期间回滚事务对等项。

节点名称标识符必须在每个事务管理器部署中保持唯一。此外,在事务管理器重新启动期间,节点标识符必须保持稳定。

节点名称标识符可以通过属性 quarkus.transaction-manager.node-name 进行配置。

Using @TransactionScoped to bind CDI beans to the transaction lifecycle

您可以定义与事务生存期一样长的 Bean,并通过 CDI 生命周期事件在事务开始和结束时执行操作。

只需使用 @TransactionScoped 标注将事务 scope 分配给此类 Bean:

@TransactionScoped
public class MyTransactionScopedBean {

    // The bean's state is bound to a specific transaction,
    // and restored even after suspending then resuming the transaction.
    int myData;

    @PostConstruct
    void onBeginTransaction() {
        // This gets invoked after a transaction begins.
    }

    @PreDestroy
    void onBeforeEndTransaction() {
        // This gets invoked before a transaction ends (commit or rollback).
    }
}

或者,如果您不一定需要在事务期间保留状态,而只是想对事务开始/结束事件作出反应,则可以简单地在范围不同的 Bean 中声明事件侦听器:

@ApplicationScoped
public class MyTransactionEventListeningBean {

    void onBeginTransaction(@Observes @Initialized(TransactionScoped.class) Object event) {
        // This gets invoked when a transaction begins.
    }

    void onBeforeEndTransaction(@Observes @BeforeDestroyed(TransactionScoped.class) Object event) {
        // This gets invoked before a transaction ends (commit or rollback).
    }

    void onAfterEndTransaction(@Observes @Destroyed(TransactionScoped.class) Object event) {
        // This gets invoked after a transaction ends (commit or rollback).
    }
}

event 对象表示事务 ID,并相应地定义 toString()/equals()/hashCode()

在侦听器方法中,您可以通过访问 TransactionManager 来访问正在进行的事务的详细信息,这是一个 CDI Bean,可以 @Inject

Configure storing of Quarkus transaction logs in a database

在持久存储不可用的云环境中(例如,当应用程序容器无法使用持久卷时),您可以通过使用 Java 数据库连接 (JDBC) 数据源将事务管理配置为将事务日志存储在数据库中。

然而,在云原生应用程序中,使用数据库存储事务日志还有其他要求。管理这些事务的 narayana-jta 扩展需要稳定的存储、一个唯一的可重复使用的节点标识符和一个稳定的 IP 地址才能正确工作。虽然 JDBC 对象存储提供稳定的存储,但用户仍然必须计划如何满足其他两个要求。

在您评估是否将数据库用于存储事务日志后,Quarkus 允许对 quarkus.transaction-manager.object-store.<property> 属性中包含的对象存储进行以下 JDBC 特定配置,其中 <property> 可以是:

  • type (string):将此属性配置为 jdbc 以启用使用 Quarkus JDBC 数据源存储事务日志。默认值为 file-system

  • datasource (string):指定用于存储事务日志的数据源的名称。如果未提供 datasource 属性的值,Quarkus 将使用 default datasource

  • create-table (boolean):如果设置为 true,则在事务日志表尚不存在时自动创建该表。默认值为 false

  • drop-table (boolean):如果设置为 true,则在表已经存在时在启动时删除这些表。默认值为 false

  • table-prefix (字符串):指定相关表名称的前缀。默认值为 quarkus_

有关更多配置信息,请参阅 Quarkus All configuration options 参考的 Narayana JTA - Transaction manager 部分。

Additional information:
  • 通过将 create-table 属性设置为 true,在初始设置期间创建事务日志表。

  • JDBC 数据源和 ActiveMQ Artemis 允许登记并自动注册 XAResourceRecovery

    • JDBC 数据源是 quarkus-agroal 的一部分,它需要使用 quarkus.datasource.jdbc.transactions=XA

    • ActiveMQ Artemis 是 quarkus-pooled-jms 的一部分,它需要使用 quarkus.pooled-jms.transaction=XA

  • 为了确保发生应用程序崩溃或故障时数据完整性,请使用 quarkus.transaction-manager.enable-recovery=true 配置启用事务崩溃恢复。

为了解决 Agroal having a different view on running transaction checks 的当前已知问题,请将负责写入事务日志的数据源的事务类型设置为 disabled

quarkus.datasource.TX_LOG.jdbc.transactions=disabled

本示例使用 TX_LOG 作为数据源名称。

Why always having a transaction manager?

Does it work everywhere I want to?

Yep, it works in your Quarkus application, in your IDE, in your tests, because all of these are Quarkus applications. JTA has some bad press for some people. I don’t know why. Let’s just say that this is not your grandpa’s JTA implementation. What we have is perfectly embeddable and lean.

Does it do 2 Phase Commit and slow down my app?

No, this is an old folk tale. Let’s assume it essentially comes for free and let you scale to more complex cases involving several datasources as needed.

I don’t need transaction when I do read only operations, it’s faster.

Wrong. First off, just disable the transaction by marking your transaction boundary with @Transactional(NOT_SUPPORTED) (or NEVER or SUPPORTS depending on the semantic you want). Second, it’s again fairy tale that not using transaction is faster. The answer is, it depends on your DB and how many SQL SELECTs you are making. No transaction means the DB does have a single operation transaction context anyway. Third, when you do several SELECTs, it’s better to wrap them in a single transaction because they will all be consistent with one another. Say your DB represents your car dashboard, you can see the number of kilometers remaining and the fuel gauge level. By reading it in one transaction, they will be consistent. If you read one and the other from two different transactions, then they can be inconsistent. It can be more dramatic if you read data related to rights and access management for example.

Why do you prefer JTA vs Hibernate’s transaction management API

Managing the transactions manually via entityManager.getTransaction().begin() and friends lead to a butt ugly code with tons of try catch finally that people get wrong. Transactions are also about JMS and other database access, so one API makes more sense.

It’s a mess because I don’t know if my Jakarta Persistence persistence unit is using JTA or Resource-level Transaction

It’s not a mess in Quarkus :) Resource-level was introduced to support Jakarta Persistence in a non-managed environment. But Quarkus is both lean and a managed environment, so we can safely always assume we are in JTA mode. The end result is that the difficulties of running Hibernate ORM + CDI + a transaction manager in Java SE mode are solved by Quarkus.