1. Обзор
API транзакций Java, более известный как JTA, – это API для управления транзакциями в Java. Это позволяет нам запускать, фиксировать и откатывать транзакции без учета ресурсов.
Истинная мощь JTA заключается в ее способности управлять несколькими ресурсами (т. е. базами данных, службами обмена сообщениями) в одной транзакции.
В этом уроке мы познакомимся с JTA на концептуальном уровне и посмотрим, как бизнес-код обычно взаимодействует с JTA.
2. Универсальный API и распределенная транзакция
JTA обеспечивает абстракцию над управлением транзакциями (начало, фиксация и откат) в бизнес-коде.
В отсутствие этой абстракции нам пришлось бы иметь дело с отдельными API-интерфейсами каждого типа ресурсов.
Например, нам нужно иметь дело с ресурсом JDBC вот так . Аналогично, ресурс JMS может иметь аналогичную, но несовместимую модель .
С помощью JTA мы можем управлять несколькими ресурсами различных типов согласованным и скоординированным образом .
В качестве API JTA определяет интерфейсы и семантику, которые будут реализованы менеджерами транзакций . Реализации предоставляются такими библиотеками, как Narayana и Bitronix .
3. Пример Настройки Проекта
Пример приложения представляет собой очень простой серверный сервис банковского приложения. У нас есть две службы: Служба банковских счетов и Служба аудита с использованием двух разных баз данных . Эти независимые базы данных должны быть скоординированы при начале транзакции, фиксации или откате .
Начнем с того, что наш пример проекта использует Spring Boot для упрощения конфигурации:
org.springframework.boot spring-boot-starter-parent 2.4.0 org.springframework.boot spring-boot-starter-jta-bitronix
Наконец, перед каждым методом тестирования мы инициализируем AUDIT_LOG с пустыми данными и базой данных УЧЕТНОЙ ЗАПИСЬЮ с 2 строками:
+-----------+----------------+ | 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); ... }
Здесь метод execute Transfer() вызывает 2 разные службы, Службу учетных записей и Службу аудита. Эти службы используют 2 разные базы данных.
Когда execute Transfer() возвращается, менеджер транзакций распознает, что это конец транзакции, и зафиксирует обе базы данных :
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 .
Теперь давайте изменим execute Transfer() для обработки транзакции вручную:
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 .