Уявіть ситуацію: у вас замовлення в інтернет-магазині, яке має одночасно:
- Зменшити кількість товару на складі.
- Списати гроші з банківської картки клієнта.
Ці операції відбуваються в двох різних системах, наприклад, одна база даних керує складом, а інша обробляє банківські операції. Як бути впевненим, що обидві операції успішно завершилися? Що якщо одна з них провалиться? Ось тут і виручає двофазний коміт.
Двофазний коміт (2PC) — це протокол для забезпечення узгодженості даних у транзакціях, що виходять за межі однієї системи. Він координує учасників (наприклад, бази даних або мікросервіси) так, щоб усі вони або підтвердили успішне виконання операції, або відкочували зміни у випадку помилки.
Два етапи двофазного коміту
Як видно з назви, процес проходить у дві фази:
Prepare Phase (Фаза підготовки):
- Координатор транзакції розсилає всім учасникам (наприклад, базам даних або мікросервісам) запит на підготовку до виконання транзакції.
- Кожний учасник виконує підготовчі дії (наприклад, резервує ресурси) і відповідає координатору:
- OK, якщо готовий виконати операцію.
- FAIL, якщо виконати операцію неможливо.
Commit Phase (Фаза підтвердження):
- Якщо ВСІ учасники відповіли OK, координатор дає команду "підтвердити" транзакцію.
- Якщо хоча б один учасник відповів FAIL, координатор дає команду "відкотити" транзакцію.
Ось коротка схема:
Координатор -----> Учасники: "Ви готові?"
Учасники -------> Координатор: "Готові/Не готові"
Координатор -----> Учасники: "Підтверджуйте/Відкочуйте"
Переваги та обмеження двофазного коміту
Давайте розберемося, що хорошого і поганого в цьому підході.
Переваги
- Узгодженість: протокол гарантує, що всі учасники в синхронізованому стані — або всі зміни підтверджені, або всі відкочені.
- Проста модель: 2PC дає простий спосіб керування транзакціями в розподілених системах.
Обмеження
- Продуктивність: фаза підготовки додає мережеві затримки. У великих системах це може стати вузьким місцем.
- Блокування ресурсів: під час виконання транзакції ресурси учасників заблоковані, що зменшує загальну продуктивність системи.
- Збій координатора: якщо координатор вийде з ладу, транзакція зависне. Так, це звучить неприємно.
Реалізація двофазного коміту в 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).
- Моніторити стан транзакцій і вчасно розбирати завислі.
На цьому завершуємо занурення в двофазний коміт. У наступній лекції обговоримо, як оптимізувати транзакції в застосунках, не жертвуючи продуктивністю.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ