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

Транзакції в розподілених системах: двофазний коміт (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).
  • Моніторити стан транзакцій і вчасно розбирати завислі.

На цьому завершуємо занурення в двофазний коміт. У наступній лекції обговоримо, як оптимізувати транзакції в застосунках, не жертвуючи продуктивністю.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ