JavaRush /Курсы /Модуль 5. Spring /Транзакции в распределённых системах: двухфазный коммит (...

Транзакции в распределённых системах: двухфазный коммит (2PC)

Модуль 5. Spring
6 уровень , 6 лекция
Открыта

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

  1. Уменьшить количество товара на складе.
  2. Списать деньги с банковской карты клиента.

Эти операции происходят в двух разных системах, например, одна база данных управляет складом, а другая обрабатывает банковские операции. Как быть уверенными, что обе операции успешно завершились? Что если одна из них провалится? Вот тут и приходит на помощь двухфазный коммит.

Двухфазный коммит (2PC) — это протокол для обеспечения согласованности данных в транзакциях, выходящих за пределы одной системы. Он координирует участников (например, базы данных или микросервисы) так, чтобы все они либо подтвердили успешное выполнение операции, либо откатили изменения в случае ошибки.


Два этапа двухфазного коммита

Как видно из названия, процесс протекает в две фазы:

  1. Prepare Phase (Фаза подготовки):

    • Координатор транзакции рассылает всем участникам (например, базам данных или микросервисам) запрос на подготовку к выполнению транзакции.
    • Каждый участник выполняет подготовительные действия (например, резервирует ресурсы) и отвечает координатору:
      • OK, если готов выполнить операцию.
      • FAIL, если выполнить операцию невозможно.
  2. Commit Phase (Фаза подтверждения):

    • Если ВСЕ участники ответили OK, координатор даёт команду "подтвердить" транзакцию.
    • Если хотя бы один участник ответил FAIL, координатор даёт команду "откатить" транзакцию.

Вот краткая схема:


Координатор -----> Участники: "Вы готовы?"
Участники -------> Координатор: "Готовы/Не готовы"
Координатор -----> Участники: "Подтверждайте/Откатывайте"

Преимущества и ограничения двухфазного коммита

Давайте разберёмся, что хорошего и плохого в этом подходе.

Преимущества

  1. Согласованность: протокол гарантирует, что все участники находятся в синхронизированном состоянии — либо все изменения подтверждены, либо все откатили свои действия.
  2. Простая модель: 2PC предоставляет простой способ управления транзакциями в распределённых системах.

Ограничения

  1. Производительность: фаза подготовки добавляет сетевые задержки. В больших системах это может стать узким местом.
  2. Блокировка ресурсов: во время выполнения транзакции ресурсы участников заблокированы, что снижает общую производительность системы.
  3. Сбой координатора: если координатор выйдет из строя, транзакция зависнет. Да, это звучит неприятно.

Реализация двухфазного коммита в Spring

Spring поддерживает двухфазный коммит через Java Transaction API (JTA). Давайте посмотрим, как это работает.

Настройка JTA в Spring Boot

Для работы с двухфазным коммитом нам понадобится JTA и поддержка нескольких источников данных. В качестве примера мы используем две базы данных: одну для управления заказами, другую для управления платежами.

Добавим зависимости JTA в pom.xml


<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
  <groupId>com.atomikos</groupId>
  <artifactId>transactions-jta</artifactId>
  <version>5.0.8</version>
</dependency>

Atomikos — это популярный менеджер транзакций, поддерживающий JTA. Он возьмёт на себя роль координатора.

Настроим два источника данных


@Configuration
public class DataSourceConfig {

    @Bean(name = "ordersDataSource")
    @Primary
    public DataSource ordersDataSource() {
        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        dataSource.setUniqueResourceName("ordersDB");
        dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        dataSource.setXaProperties(createDatabaseProperties("jdbc:mysql://localhost:3306/orders", "root", "password"));
        return dataSource;
    }

    @Bean(name = "paymentsDataSource")
    public DataSource paymentsDataSource() {
        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        dataSource.setUniqueResourceName("paymentsDB");
        dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        dataSource.setXaProperties(createDatabaseProperties("jdbc:mysql://localhost:3306/payments", "root", "password"));
        return dataSource;
    }

    private Properties createDatabaseProperties(String url, String username, String password) {
        Properties properties = new Properties();
        properties.setProperty("user", username);
        properties.setProperty("password", password);
        properties.setProperty("url", url);
        return properties;
    }
}

Определяем TransactionManager

Совет:

Spring автоматически распознаёт JTA-базы данных, если они правильно настроены.


Пример двухфазного транзакционного метода

Теперь у нас есть два источника данных, и мы можем писать транзакционные методы, которые будут работать с ними одновременно.


@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private PaymentRepository paymentRepository;

    @Transactional
    public void createOrderAndProcessPayment(Order order, Payment payment) {
        // Сохраняем заказ в одной базе данных
        orderRepository.save(order);

        // Обрабатываем платёж в другой базе данных
        paymentRepository.save(payment);

        // Если что-то пошло не так, транзакция будет автоматически откатана
        if (payment.getAmount() > 1000) {
            throw new IllegalArgumentException("Слишком большая сумма!");
        }
    }
}

Когда использовать двухфазный коммит?

Двухфазный коммит хорошо работает в случаях, когда:

  • У вас есть жёсткие требования к согласованности данных.
  • Транзакции затрагивают несколько систем или баз данных.

Однако следует учитывать его ограничения: высокая стоимость и уязвимость к сбоям. Если согласованность не так критична, можно рассмотреть более лёгкие альтернативы, такие как событийно-ориентированная архитектура (EDA) — об этом мы поговорим позже в курсе.


Типичные ошибки и как их избежать

Самая распространённая ошибка — не учитывать вероятность сбоя координатора. В этом случае транзакция может оставаться в подвешенном состоянии, а заблокированные ресурсы создавать проблемы.

Чтобы избежать этого, важно:

  • Использовать надёжные координационные менеджеры транзакций (например, Atomikos).
  • Мониторить состояние транзакций и своевременно разбирать зависшие.

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

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ