The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

在Quarkus中使用事务

Quarkus自带一个事务管理器,并使用它来协调和暴露事务给你的应用程序。每个处理持久性的扩展都会与它集成。而且你将通过CDI明确地与事务进行交互。本指南将指导你完成这一切。

设置它

在大多数情况下,你不需要担心设置它,因为需要它的扩展会简单地把它作为一个依赖项加入。例如,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")

开始和停止事务:确定你的事务边界

You can define your transaction boundaries either declaratively with @Transactional or programmatically with QuarkusTransaction. You can also use the JTA UserTransaction API directly, however this is less user-friendly than QuarkusTransaction.

声明式方法

定义你的事务边界的最简单的方法是在你的入口方法上使用 @Transactional 注释( javax.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 A RuntimeException crossing the transaction boundaries will roll back the transaction.

@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 以编程方式决定将该事务设置为回滚。

事务配置

除了在你的入口方法上或在类的层面上设置标准的 @Transactional 注解之外,事务的高级配置还可以通过使用 @TransactionConfiguration 注解来实现。

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

这个注释只能放在划定事务的顶层方法上。一旦事务开始,被注解的嵌套方法将抛出一个异常。

如果在一个类上定义,就相当于在该类所有标有 @Transactional 的方法上定义了它。在方法上定义的配置优先于在类上定义的配置。

响应性扩展

如果你的 @Transactional -注释的方法返回一个响应性的值,例如:

  • CompletionStage (来自JDK)

  • Publisher (来自Reactive-Streams)

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

那么行为就有点不同了,因为在返回的响应式值终止之前,事务不会被终止。实际上,返回的响应式值将被监听,如果它意外终止,事务将被标记为回滚,并且只有在响应式值终止时才会提交或回滚。

这允许你的响应式方法以异步方式继续在事务上工作,直到他们的工作真正完成,而不仅仅是当响应式方法返回时。

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

编程实现方法

你可以使用 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 lambdaExample() {
        QuarkusTransaction.run(() -> {
            //do work
        });


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

上面的例子显示了几种不同的API使用方法。第一个方法是简单地调用begin,做一些工作并提交。这个创建的事务是与CDI请求范围相联系的,所以如果它在请求范围被破坏时仍处于活动状态,那么它将被自动回滚。这样就不需要明确地捕捉异常和调用 rollback ,并作为一个安全措施来防止无意中的事务泄露,然而这也意味着这只能在请求范围处于活动状态时使用。方法调用中的第二个例子以一个超时选项开始,然后回滚事务。

第二个例子展示了lambda范围内事务的使用,第一个例子只是在一个事务中运行 Runnable ,第二个例子,运行 Callable ,并带有一些特定的选项。特别是 exceptionHandler 方法可以用来控制事务是否在异常情况下回滚,而 semantic 方法可以控制现有事务已经开始时的行为。

支持以下语义:

DISALLOW_EXISTING

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

JOIN_EXISTING

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

REQUIRE_NEW

这是默认的语义。如果一个现有的事务已经与当前线程相关联,那么该事务将被暂停,并在当前事务完成后恢复。一个新的事务在现有事务被暂停后开始,并遵循所有正常的生命周期规则。

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 中了解更多信息。

您还可以提供以数字开头的持续时间值。 在这种情况下,如果该值仅包含一个数字,则转换器将该值视为秒。 否则,PT 会隐式添加到值的前面,以获得标准的 java.time.Duration 格式。

默认值是60秒。

配置事务节点名称标识符

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

节点名称标识符在交易的识别中起着至关重要的作用。当交易被创建时,节点名称标识符被做为成交易ID的一部分。基于节点名称标识符,事务管理器能够识别在数据库或JMS代理中创建的XA事务对应物。该标识符使事务管理器有可能在恢复期间回滚事务对应方。

节点名称标识符需要在每个事务管理器部署中是唯一的。而且节点标识符需要在事务管理器重新启动时保持不变。

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

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

它能在我希望的所有地方工作吗?

是的,它在你的Quarkus应用程序中,在你的IDE中,在你的测试中都可以使用,因为这些都是Quarkus应用程序。JTA对一些人来说有一些不好的影响。我不知道为什么。我们只能说,这不是老旧的JTA实现。我们所拥有的是完美的可嵌入的和精简的实现。

两阶段提交是否会减慢了我的应用程序?

不,这是一个古老的民间故事。让我们假设它基本上是免费的,让你根据需要扩展到涉及几个数据源的更复杂的情况。

当我做只读操作时,就不需要事务,这样会更快

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.

为什么相对Hibernate 的事务管理 API你更推荐JTA

通过 entityManager.getTransaction().begin() 和手动管理事务会导致一段丑陋的代码,最终人们会出错。 事务也与 JMS 和其他数据库访问有关,因此一个 API 更有意义。

当不知道JPA persistence unit 使用的是“JTA”还是“Resource-level”事务,这可能会造成混乱

It’s not a mess in Quarkus :) Resource-level was introduced to support JPA 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.