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 .