1. 概述
Java Transaction API(简称 JTA)是用于在 Java 中管理事务的一套 API。它提供了一种资源无关的方式来启动、提交和回滚事务。
JTA 的核心价值在于它能够在一个事务中协调多个资源(例如数据库、消息队列等),实现分布式事务的统一管理。
在本教程中,我们将从概念层面了解 JTA,并通过常见业务代码示例展示如何与 JTA 进行交互。
2. 统一 API 与分布式事务
JTA 提供了一层事务控制的抽象,使得业务代码可以以统一的方式处理事务的开始、提交和回滚操作。
如果没有这一层抽象,我们就需要直接面对不同资源的事务 API。比如:
- 对于 JDBC 资源,我们需要手动处理事务
- 对于 JMS 资源,则可能需要使用另一套不兼容的事务模型
✅ 有了 JTA,我们就可以 以一致、协调的方式管理多种不同类型的资源。
作为 API,JTA 定义了接口和语义规范,具体的实现由事务管理器(Transaction Manager)来完成。常见的实现库包括:
3. 示例项目搭建
本示例项目是一个银行应用的简单后端服务,其中包含两个服务:BankAccountService
和 AuditService
,分别使用两个不同的数据库。
⚠️ 这两个独立的数据库需要在事务开始、提交或回滚时进行协调。
项目使用 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);
...
}
在这个方法中,调用了两个不同的服务:AccountService
和 AuditService
,它们分别操作两个不同的数据库。
当 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 项目地址。