在 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,您可能需要显式地将其添加为依赖项。将以下内容添加到您的构建文件中
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-narayana-jta</artifactId>
</dependency>
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)
:如果启动了事务,则引发异常;否则在没有事务的情况下工作。
REQUIRED
或 NOT_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,或者通过使用显式的 begin
、commit
和 rollback
方法。
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();
}
}
}
您不能在使用 |
配置事务超时
您可以通过属性 quarkus.transaction-manager.default-transaction-timeout
配置默认事务超时,即应用于事务管理器管理的所有事务的超时,该属性指定为持续时间。
要写入持续时间值,请使用标准 您还可以使用简化的格式,以数字开头
在其他情况下,简化格式将被转换为
|
默认值为 60 秒。
配置事务节点名称标识符
作为底层事务管理器的 Narayana 具有唯一节点标识符的概念。如果您考虑使用涉及多个资源的 XA 事务,这很重要。
节点名称标识符在事务识别中起着至关重要的作用。创建事务时,节点名称标识符会被伪造到事务 ID 中。基于节点名称标识符,事务管理器能够识别在数据库或 JMS 代理中创建的 XA 事务对应项。该标识符使事务管理器能够在恢复期间回滚事务对应项。
每个事务管理器部署都需要唯一的节点名称标识符。并且节点标识符需要在事务管理器重新启动后保持稳定。
可以通过属性 quarkus.transaction-manager.node-name
配置节点名称标识符。
节点名称不能超过 28 个字节。要自动缩短超过 28 个字节的名称,请将 缩短是通过散列节点名称、将散列编码为 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 对运行事务检查有不同的看法,请将负责写入事务日志的数据源的数据源事务类型设置为 quarkus.datasource.TX_LOG.jdbc.transactions=disabled 此示例使用 TX_LOG 作为数据源名称。 |
为什么总是要有一个事务管理器?
- 它在我想要的地方都有效吗?
-
是的,它在您的 Quarkus 应用程序、您的 IDE、您的测试中都有效,因为所有这些都是 Quarkus 应用程序。 JTA 对某些人来说有一些不好的评价。我不知道为什么。让我们说这不是您祖父的 JTA 实现。我们拥有的东西是完全可嵌入和精简的。
- 它会执行 2 阶段提交并降低我的应用程序的速度吗?
-
不,这是一个古老的民间传说。让我们假设它本质上是免费的,并且让您可以根据需要扩展到涉及多个数据源的更复杂的情况。
- 当我执行只读操作时,我不需要事务,它更快。
-
错误的。
首先,只需通过使用@Transactional(NOT_SUPPORTED)
(或NEVER
或SUPPORTS
,具体取决于您想要的语义)标记您的事务边界来禁用事务。
其次,不使用事务更快,这又是天方夜谭。答案是,这取决于您的数据库以及您发出的 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 + 事务管理器的难题。