Перейти к основному содержимому

Путеводитель по Джакарте EE JTA

· 4 мин. чтения

1. Обзор

Java Transaction API, более известный как JTA, представляет собой API для управления транзакциями в Java. Это позволяет нам запускать, фиксировать и откатывать транзакции независимо от ресурсов.

Истинная сила JTA заключается в его способности управлять несколькими ресурсами (например, базами данных, службами обмена сообщениями) в рамках одной транзакции.

В этом руководстве мы познакомимся с JTA на концептуальном уровне и увидим, как бизнес-код обычно взаимодействует с JTA.

2. Универсальный API и распределенная транзакция

JTA обеспечивает абстракцию управления транзакциями (начало, фиксация и откат) для бизнес-кода.

В отсутствие этой абстракции нам пришлось бы иметь дело с отдельными API каждого типа ресурсов.

Например, нам нужно иметь дело с ресурсом JDBC следующим образом . Точно так же ресурс JMS может иметь аналогичную, но несовместимую модель .

С помощью JTA мы можем управлять несколькими ресурсами разных типов согласованным и скоординированным образом .

В качестве API JTA определяет интерфейсы и семантику, которые должны быть реализованы менеджерами транзакций . Реализации предоставляются такими библиотеками, как Narayana и Atomikos .

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 . Более подробное объяснение и настройку смотрите в этой статье .

Давайте аннотируем метод фасадного сервиса executeTranser() с помощью @Transactional. Это указывает менеджеру транзакций начать транзакцию :

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

Здесь метод executeTranser() вызывает 2 разные службы: AccountService и AuditService. Эти сервисы используют 2 разные базы данных.

Когда 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 после первого @Transactional приведет к откату транзакции в обеих базах данных . По сути, выполнение перевода на сумму, превышающую баланс, вызовет откат :

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. Программная демаркация транзакций

Другой способ управления транзакцией JTA — программно через 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. Примеры кода иллюстрируют управление транзакциями с помощью аннотаций и программно , включая 2 транзакционных ресурса, которые необходимо координировать в одной транзакции.

Как обычно, пример кода можно найти на GitHub .