编辑此页面

在 Quarkus 中使用事务

quarkus-narayana-jta 扩展提供了一个事务管理器,用于协调事务并将事务暴露给您的应用程序,如链接中所述:Jakarta Transactions 规范(以前称为 Java Transaction API (JTA))。

在讨论 Quarkus 事务时,本指南指的是 Jakarta Transactions 事务风格,并且仅使用术语事务来指代它们。

此外,Quarkus 不支持分布式事务。这意味着诸如 Java Transaction Service (JTS)、REST-AT、WS-Atomic Transaction 等传播事务上下文的模型,narayana-jta 扩展不支持。

设置

大多数时候您不需要担心设置它,因为需要它的扩展程序会简单地将其添加为依赖项。例如,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")

启动和停止事务:定义您的边界

您可以使用 @Transactional 以声明方式或使用 QuarkusTransaction 以编程方式定义事务边界。您也可以直接使用 JTA UserTransaction API,但这不如 QuarkusTransaction 那么用户友好。

声明式方法

定义事务边界的最简单方法是在您的入口方法(jakarta.transaction.Transactional)上使用 @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 以编程方式决定将事务设置为回滚。

事务配置

通过使用 @TransactionConfiguration 注解可以进行事务的高级配置,该注解除了在您的入口方法上或在类级别上设置的标准 @Transactional 注解之外。

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

此注解只能放在限定事务的顶级方法上。一旦事务启动,带注解的嵌套方法将抛出异常。

如果在类上定义,则相当于在类中所有标记有 @Transactional 的方法上定义它。在方法上定义的配置优先于在类上定义的配置。

响应式扩展

如果您的 @Transactional 注解方法返回一个响应式值,例如

  • CompletionStage(来自 JDK)

  • Publisher(来自 Reactive-Streams)

  • 可以使用响应式类型转换器转换为前两种类型之一的任何类型

那么行为会有点不同,因为只有在返回的响应式值终止后,事务才会终止。实际上,将侦听返回的响应式值,如果它异常终止,则事务将被标记为回滚,并且仅在响应式值终止时才会被提交或回滚。

这允许您的响应式方法异步地继续处理事务,直到它们的工作真正完成,而不仅仅是直到响应式方法返回。

如果您需要在您的响应式管道中传播您的事务上下文,请参阅上下文传播指南

编程方法

您可以使用 QuarkusTransaction 上的静态方法来定义事务边界。这提供了两种不同的选项,一种函数式方法,允许您在事务范围内运行 lambda,或者通过使用显式的 begincommitrollback 方法。

import io.quarkus.narayana.jta.QuarkusTransaction;

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 TransactionExceptionResult.COMMIT;
                    }
                    return TransactionExceptionResult.ROLLBACK;
                })
                .call(() -> {
                    //do work
                    return 0;
                });
    }
}

上面的示例展示了 API 可以使用的几种不同方式。

第一个方法只是调用 begin,做一些工作并提交它。此创建的事务与 CDI 请求范围相关联,因此如果在请求范围被销毁时它仍然处于活动状态,那么它将自动回滚。这消除了显式捕获异常并调用 rollback 的需要,并且可以作为防止意外事务泄漏的安全网,但是这意味着这只能在请求范围处于活动状态时使用。该方法中的第二个示例调用带有超时选项的 begin,然后回滚事务。

第二种方法展示了使用 lambda 范围事务与 QuarkusTransaction.runner(…​);第一个示例只是在新的事务中运行一个 Runnable,第二个示例做同样的事情,但是加入现有的事务(如果有),第三个示例调用带有某些特定选项的 Callable。特别是,exceptionHandler 方法可用于控制在异常时是否回滚事务。

支持以下语义

QuarkusTransaction.disallowingExisting()/DISALLOW_EXISTING

如果事务已经与当前线程关联,则将抛出 QuarkusTransactionException,否则将启动一个新事务,并遵循所有正常的生命周期规则。

QuarkusTransaction.joiningExisting()/JOIN_EXISTING

如果未激活任何事务,则将启动一个新事务,并在方法结束时提交。如果抛出异常,则将调用 #exceptionHandler(Function) 注册的异常处理程序来决定是否应该提交或回滚 TX。如果现有事务处于活动状态,则该方法将在现有事务的上下文中运行。如果抛出异常,则将调用异常处理程序,但是 ExceptionResult#ROLLBACK 的结果将导致 TX 标记为仅回滚,而 ExceptionResult#COMMIT 的结果将导致不采取任何操作。

QuarkusTransaction.requiringNew()/REQUIRE_NEW

如果现有事务已经与当前线程关联,则该事务将被挂起,然后启动一个新事务,该事务遵循所有正常的生命周期规则,并且当它完成时,原始事务将被恢复。否则,将启动一个新事务,并遵循所有正常的生命周期规则。

QuarkusTransaction.suspendingExisting()/SUSPEND_EXISTING

如果没有激活任何事务,那么这些语义基本上是无操作的。如果事务处于活动状态,则在任务运行后将其挂起并恢复。使用这些语义时,永远不会咨询异常处理程序,同时指定异常处理程序和这些语义被认为是错误的。这些语义允许代码轻松地在事务范围之外运行。

旧版 API 方法

不太简单的方法是注入一个 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

配置事务超时

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

要写入持续时间值,请使用标准 java.time.Duration 格式。有关更多信息,请参阅 Duration#parse() javadoc

您还可以使用简化的格式,以数字开头

  • 如果该值仅为一个数字,则表示以秒为单位的时间。

  • 如果该值是一个数字后跟 ms,则表示以毫秒为单位的时间。

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

  • 如果该值是一个数字后跟 hms,则在其前面加上 PT

  • 如果该值是一个数字后跟 d,则在其前面加上 P

默认值为 60 秒。

配置事务节点名称标识符

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

节点名称标识符在事务识别中起着至关重要的作用。创建事务时,节点名称标识符会被伪造到事务 ID 中。基于节点名称标识符,事务管理器能够识别在数据库或 JMS 代理中创建的 XA 事务对应项。该标识符使事务管理器能够在恢复期间回滚事务对应项。

每个事务管理器部署都需要唯一的节点名称标识符。并且节点标识符需要在事务管理器重新启动后保持稳定。

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

节点名称不能超过 28 个字节。要自动缩短超过 28 个字节的名称,请将 quarkus.transaction-manager.shorten-node-name-if-necessary 设置为 true

缩短是通过散列节点名称、将散列编码为 Base64 然后截断结果来实现的。与所有散列一样,生成的缩短节点名称可能会与另一个缩短节点名称冲突,但这非常不可能

使用 @TransactionScoped 将 CDI bean 绑定到事务生命周期

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

只需使用 @TransactionScoped 注解将事务范围分配给此类 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 来访问有关正在进行的事务的更多信息,TransactionManager 是一个 CDI bean 并且可以被 @Inject

配置在数据库中存储 Quarkus 事务日志

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

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

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

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

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

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

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

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

有关更多配置信息,请参阅 Quarkus 的Narayana JTA - 事务管理器部分所有配置选项参考。

附加信息
  • 通过将 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 对运行事务检查有不同的看法,请将负责写入事务日志的数据源的数据源事务类型设置为 disabled

quarkus.datasource.TX_LOG.jdbc.transactions=disabled

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

为什么总是要有一个事务管理器?

它在我想要的地方都有效吗?

是的,它在您的 Quarkus 应用程序、您的 IDE、您的测试中都有效,因为所有这些都是 Quarkus 应用程序。 JTA 对某些人来说有一些不好的评价。我不知道为什么。让我们说这不是您祖父的 JTA 实现。我们拥有的东西是完全可嵌入和精简的。

它会执行 2 阶段提交并降低我的应用程序的速度吗?

不,这是一个古老的民间传说。让我们假设它本质上是免费的,并且让您可以根据需要扩展到涉及多个数据源的更复杂的情况。

当我执行只读操作时,我不需要事务,它更快。

错误的。
首先,只需通过使用 @Transactional(NOT_SUPPORTED)(或 NEVERSUPPORTS,具体取决于您想要的语义)标记您的事务边界来禁用事务。
其次,不使用事务更快,这又是天方夜谭。答案是,这取决于您的数据库以及您发出的 SQL SELECT 数量。无论如何,没有事务意味着 DB 确实具有单一操作事务上下文。
第三,当您执行多个 SELECT 时,最好将它们包装在一个事务中,因为它们将彼此一致。假设您的数据库代表您的汽车仪表板,您可以看到剩余的公里数和燃油表级别。通过在一个事务中读取它,它们将是一致的。如果您从两个不同的事务中读取一个和另一个,那么它们可能会不一致。如果您读取与权利和访问管理相关的数据,则可能会更加引人注目。

为什么您更喜欢 JTA 而不是 Hibernate 的事务管理 API

通过 entityManager.getTransaction().begin() 及其朋友手动管理事务会导致丑陋的代码,其中包含大量人们出错的 try catch finally。事务也与 JMS 和其他数据库访问有关,因此一个 API 更有意义。

这是一个烂摊子,因为我不知道我的 Jakarta Persistence 持久化单元使用的是 JTA 还是 Resource-level 事务

在 Quarkus 中这不是一个烂摊子 :) 引入资源级别是为了在非托管环境中支持 Jakarta Persistence。但是 Quarkus 既精简又是托管环境,因此我们可以安全地始终假设我们处于 JTA 模式。最终结果是 Quarkus 解决了在 Java SE 模式下运行 Hibernate ORM + CDI + 事务管理器的难题。

相关内容