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

Программное управление транзакциями в Spring

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

Задача: Наибольшая подстрока палиндром

Для заданной строки s, верните наибольшую подстроку палиндром входящую в s. Подстрока — это непрерывная непустая последовательность символов внутри строки. Стока является палиндромом, если она читается одинаково в обоих направлениях...

ANDROMEDA 42

1. Обзор

Аннотация Spring @Transactional предоставляет хороший декларативный API для обозначения границ транзакций.

За кулисами аспект заботится о создании и обслуживании транзакций, поскольку они определены в каждом экземпляре аннотации @Transactional . Такой подход позволяет легко отделить нашу основную бизнес -логику от сквозных задач, таких как управление транзакциями.

В этом уроке мы увидим, что это не всегда лучший подход. Мы рассмотрим, какие программные альтернативы предоставляет Spring, например TransactionTemplate , и причины их использования.

2. Проблемы в раю

Предположим, мы смешиваем два разных типа ввода-вывода в простом сервисе:

@Transactional
public void initialPayment(PaymentRequest request) {
savePaymentRequest(request); // DB
callThePaymentProviderApi(request); // API
updatePaymentState(request); // DB
saveHistoryForAuditing(request); // DB
}

Здесь у нас есть несколько вызовов базы данных наряду с, возможно, дорогостоящим вызовом REST API. На первый взгляд, может иметь смысл сделать весь метод транзакционным, поскольку мы можем захотеть использовать один EntityManager для атомарного выполнения всей операции.

Однако, если ответ этого внешнего API занимает больше времени, чем обычно, по какой-либо причине, у нас скоро могут закончиться соединения с базой данных!

2.1. Суровая природа реальности

Вот что происходит, когда мы вызываем метод initialPayment :

  1. Транзакционный аспект создает новый EntityManager и запускает новую транзакцию — таким образом, он заимствует одно соединение из пула соединений.
  2. После первого вызова базы данных он вызывает внешний API, сохраняя заимствованное соединение .
  3. Наконец, он использует это соединение для выполнения оставшихся вызовов базы данных.

Если вызов API какое-то время отвечает очень медленно, этот метод перехватит заимствованное соединение , ожидая ответа .

Представьте, что в этот период мы получаем всплеск обращений к методу initialPayment . Затем все соединения могут ожидать ответа от вызова API. Вот почему у нас могут закончиться соединения с базой данных — из-за медленной серверной службы!

Смешивание ввода-вывода базы данных с другими типами ввода-вывода в транзакционном контексте — неприятный запах. Таким образом, первое решение подобных проблем — полностью разделить эти типы ввода-вывода . Если по какой-либо причине мы не можем их разделить, мы все равно можем использовать Spring API для управления транзакциями вручную.

3. Использование шаблона транзакции

TransactionTemplate предоставляет набор API-интерфейсов на основе обратного вызова для ручного управления транзакциями. Чтобы использовать его, во-первых, мы должны инициализировать его с помощью PlatformTransactionManager.

Например, мы можем настроить этот шаблон, используя внедрение зависимостей:

// test annotations
class ManualTransactionIntegrationTest {

@Autowired
private PlatformTransactionManager transactionManager;

private TransactionTemplate transactionTemplate;

@BeforeEach
void setUp() {
transactionTemplate = new TransactionTemplate(transactionManager);
}

// omitted
}

PlatformTransactionManager помогает шаблону создавать, фиксировать или откатывать транзакции.

При использовании Spring Boot соответствующий bean-компонент типа PlatformTransactionManager будет автоматически зарегистрирован, поэтому нам просто нужно его внедрить. В противном случае мы должны вручную зарегистрировать bean -компонент PlatformTransactionManager .

3.1. Пример модели предметной области

С этого момента для демонстрации мы будем использовать упрощенную модель платежного домена. В этом простом домене у нас есть объект Payment для инкапсуляции деталей каждого платежа:

@Entity
public class Payment {

@Id
@GeneratedValue
private Long id;

private Long amount;

@Column(unique = true)
private String referenceNumber;

@Enumerated(EnumType.STRING)
private State state;

// getters and setters

public enum State {
STARTED, FAILED, SUCCESSFUL
}
}

Кроме того, мы будем запускать все тесты внутри тестового класса, используя библиотеку Testcontainers для запуска экземпляра PostgreSQL перед каждым тестовым случаем:

@DataJpaTest
@Testcontainers
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = NONE)
@Transactional(propagation = NOT_SUPPORTED) // we're going to handle transactions manually
public class ManualTransactionIntegrationTest {

@Autowired
private PlatformTransactionManager transactionManager;

@Autowired
private EntityManager entityManager;

@Container
private static PostgreSQLContainer<?> pg = initPostgres();

private TransactionTemplate transactionTemplate;

@BeforeEach
public void setUp() {
transactionTemplate = new TransactionTemplate(transactionManager);
}

// tests

private static PostgreSQLContainer<?> initPostgres() {
PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:11.1")
.withDatabaseName("foreach")
.withUsername("test")
.withPassword("test");
pg.setPortBindings(singletonList("54320:5432"));

return pg;
}
}

3.2. Транзакции с результатами

TransactionTemplate предлагает метод execute , который может запустить любой блок кода внутри транзакции, а затем вернуть некоторый результат : ``

@Test
void givenAPayment_WhenNotDuplicate_ThenShouldCommit() {
Long id = transactionTemplate.execute(status -> {
Payment payment = new Payment();
payment.setAmount(1000L);
payment.setReferenceNumber("Ref-1");
payment.setState(Payment.State.SUCCESSFUL);

entityManager.persist(payment);

return payment.getId();
});

Payment payment = entityManager.find(Payment.class, id);
assertThat(payment).isNotNull();
}

Здесь мы сохраняем новый экземпляр Payment в базе данных, а затем возвращаем его автоматически сгенерированный идентификатор.

Подобно декларативному подходу, шаблон может гарантировать нам атомарность. То есть, если одна из операций внутри транзакции не завершается, она `` откатывает их все:

@Test
void givenTwoPayments_WhenRefIsDuplicate_ThenShouldRollback() {
try {
transactionTemplate.execute(status -> {
Payment first = new Payment();
first.setAmount(1000L);
first.setReferenceNumber("Ref-1");
first.setState(Payment.State.SUCCESSFUL);

Payment second = new Payment();
second.setAmount(2000L);
second.setReferenceNumber("Ref-1"); // same reference number
second.setState(Payment.State.SUCCESSFUL);

entityManager.persist(first); // ok
entityManager.persist(second); // fails

return "Ref-1";
});
} catch (Exception ignored) {}

assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty();
}

Поскольку второй referenceNumber является дубликатом, база данных отклоняет вторую операцию сохранения, что приводит к откату всей транзакции. Поэтому в базе нет никаких платежей после транзакции. Также можно вручную инициировать откат, вызвав setRollbackOnly() для TransactionStatus :

@Test
void givenAPayment_WhenMarkAsRollback_ThenShouldRollback() {
transactionTemplate.execute(status -> {
Payment payment = new Payment();
payment.setAmount(1000L);
payment.setReferenceNumber("Ref-1");
payment.setState(Payment.State.SUCCESSFUL);

entityManager.persist(payment);
status.setRollbackOnly();

return payment.getId();
});

assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty();
}

3.3. Транзакции без результатов

Если мы не собираемся ничего возвращать из транзакции, мы можем использовать класс обратного вызова TransactionCallbackWithoutResult :

@Test
void givenAPayment_WhenNotExpectingAnyResult_ThenShouldCommit() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
Payment payment = new Payment();
payment.setReferenceNumber("Ref-1");
payment.setState(Payment.State.SUCCESSFUL);

entityManager.persist(payment);
}
});

assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1);
}

3.4. Пользовательские конфигурации транзакций

До сих пор мы использовали TransactionTemplate с конфигурацией по умолчанию. Хотя в большинстве случаев этого значения по умолчанию более чем достаточно, все же можно изменить параметры конфигурации.

Например, мы можем установить уровень изоляции транзакции :

transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);

Точно так же мы можем изменить поведение распространения транзакции:

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

Или мы можем установить тайм-аут в секундах для транзакции:

transactionTemplate.setTimeout(1000);

Можно даже извлечь выгоду из оптимизации для транзакций только для чтения:

transactionTemplate.setReadOnly(true);

В любом случае, как только мы создадим TransactionTemplate с конфигурацией, все транзакции будут использовать эту конфигурацию для выполнения. Итак, если нам нужно несколько конфигураций, мы должны создать несколько экземпляров шаблона .

4. Использование PlatformTransactionManager

В дополнение к TransactionTemplate мы можем использовать API еще более низкого уровня, например PlatformTransactionManager , для управления транзакциями вручную. Довольно интересно, что и @Transactional , и TransactionTemplate используют этот API для внутреннего управления своими транзакциями.

4.1. Настройка транзакций

Прежде чем использовать этот API, мы должны определить, как будет выглядеть наша транзакция. Например, мы можем установить трехсекундный тайм-аут с уровнем изоляции повторяемой транзакции чтения:

DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
definition.setTimeout(3);

Определения транзакций аналогичны конфигурациям TransactionTemplate . Однако мы можем использовать несколько определений только с одним PlatformTransactionManager .

4.2. Ведение транзакций

После настройки нашей транзакции мы можем программно управлять транзакциями:

@Test
void givenAPayment_WhenUsingTxManager_ThenShouldCommit() {

// transaction definition

TransactionStatus status = transactionManager.getTransaction(definition);
try {
Payment payment = new Payment();
payment.setReferenceNumber("Ref-1");
payment.setState(Payment.State.SUCCESSFUL);

entityManager.persist(payment);
transactionManager.commit(status);
} catch (Exception ex) {
transactionManager.rollback(status);
}

assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1);
}

5. Вывод

В этом руководстве мы, во-первых, увидели, когда следует выбирать программное управление транзакциями вместо декларативного подхода. Затем, представив два разных API, мы научились вручную создавать, фиксировать или откатывать любую данную транзакцию.

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