1. 概述

Java Transaction API(简称 JTA)是用于在 Java 中管理事务的一套 API。它提供了一种资源无关的方式来启动、提交和回滚事务。

JTA 的核心价值在于它能够在一个事务中协调多个资源(例如数据库、消息队列等),实现分布式事务的统一管理。

在本教程中,我们将从概念层面了解 JTA,并通过常见业务代码示例展示如何与 JTA 进行交互。

2. 统一 API 与分布式事务

JTA 提供了一层事务控制的抽象,使得业务代码可以以统一的方式处理事务的开始、提交和回滚操作。

如果没有这一层抽象,我们就需要直接面对不同资源的事务 API。比如:

✅ 有了 JTA,我们就可以 以一致、协调的方式管理多种不同类型的资源

作为 API,JTA 定义了接口和语义规范,具体的实现由事务管理器(Transaction Manager)来完成。常见的实现库包括:

3. 示例项目搭建

本示例项目是一个银行应用的简单后端服务,其中包含两个服务:BankAccountServiceAuditService,分别使用两个不同的数据库。

⚠️ 这两个独立的数据库需要在事务开始、提交或回滚时进行协调。

项目使用 Spring Boot 简化配置:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.6</version>
</parent>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

在每个测试方法执行前,我们会初始化 AUDIT_LOG 表为空,并向 ACCOUNT 表插入两条数据:

+-----------+----------------+
| ID        |  BALANCE       |
+-----------+----------------+
| a0000001  |  1000          |  
| a0000002  |  2000          |
+-----------+----------------+

4. 声明式事务划分

使用 JTA 的第一种方式是通过 @Transactional 注解来控制事务。关于更详细的配置说明,可参考 这篇文章

我们给门面服务方法 executeTransfer() 添加 @Transactional 注解,指示事务管理器开启一个新的事务:

@Transactional
public void executeTransfer(String fromAccontId, String toAccountId, BigDecimal amount) {
    bankAccountService.transfer(fromAccontId, toAccountId, amount);
    auditService.log(fromAccontId, toAccountId, amount);
    ...
}

在这个方法中,调用了两个不同的服务:AccountServiceAuditService,它们分别操作两个不同的数据库。

executeTransfer() 方法执行完毕后,✅ 事务管理器会识别到事务结束,并提交两个数据库的更改

tellerService.executeTransfer("a0000001", "a0000002", BigDecimal.valueOf(500));
assertThat(accountService.balanceOf("a0000001"))
  .isEqualByComparingTo(BigDecimal.valueOf(500));        
assertThat(accountService.balanceOf("a0000002"))
  .isEqualByComparingTo(BigDecimal.valueOf(2500));

TransferLog lastTransferLog = auditService
  .lastTransferLog();
assertThat(lastTransferLog)
  .isNotNull();        
assertThat(lastTransferLog.getFromAccountId())
  .isEqualTo("a0000001");
assertThat(lastTransferLog.getToAccountId())
  .isEqualTo("a0000002"); 
assertThat(lastTransferLog.getAmount())
  .isEqualByComparingTo(BigDecimal.valueOf(500));

4.1. 声明式事务回滚

在方法结束前,executeTransfer() 会检查账户余额,如果余额不足则抛出 RuntimeException

@Transactional
public void executeTransfer(String fromAccontId, String toAccountId, BigDecimal amount) {
    bankAccountService.transfer(fromAccontId, toAccountId, amount);
    auditService.log(fromAccontId, toAccountId, amount);
    BigDecimal balance = bankAccountService.balanceOf(fromAccontId);
    if(balance.compareTo(BigDecimal.ZERO) < 0) {
        throw new RuntimeException("Insufficient fund.");
    }
}

⚠️ 一个未被捕获的 RuntimeException 会导致事务在两个数据库上都进行回滚。因此,如果转账金额超过余额,事务会被回滚:

assertThatThrownBy(() -> {
    tellerService.executeTransfer("a0000002", "a0000001", BigDecimal.valueOf(10000));
}).hasMessage("Insufficient fund.");

assertThat(accountService.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(1000));
assertThat(accountService.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2000));
assertThat(auditServie.lastTransferLog()).isNull();

5. 编程式事务划分

除了使用注解,我们还可以通过 UserTransaction 接口以编程方式控制事务。

现在我们手动修改 executeTransfer() 方法来控制事务:

userTransaction.begin();
 
bankAccountService.transfer(fromAccontId, toAccountId, amount);
auditService.log(fromAccontId, toAccountId, amount);
BigDecimal balance = bankAccountService.balanceOf(fromAccontId);
if(balance.compareTo(BigDecimal.ZERO) < 0) {
    userTransaction.rollback();
    throw new RuntimeException("Insufficient fund.");
} else {
    userTransaction.commit();
}

在这个示例中:

  • begin() 方法开启一个新的事务
  • 如果余额不足,调用 rollback() 回滚两个数据库的更改
  • 否则,✅ 调用 commit() 提交两个数据库的更改

⚠️ 注意:无论是 commit() 还是 rollback(),都会结束当前事务。

通过编程式事务控制,我们可以获得更灵活、更精细的事务管理能力。

6. 总结

本文介绍了 JTA 要解决的核心问题,并通过代码示例展示了如何通过注解和编程方式控制事务,协调两个不同的事务性资源。

✅ 无论你是使用声明式事务还是编程式事务,JTA 都能帮你搞定分布式事务的复杂性。

如需获取完整代码示例,可以访问 GitHub 项目地址


原始标题:Guide to Jakarta EE JTA | Baeldung