Представьте себе ситуацию: у вас заказ в интернет-магазине, который должен одновременно:
- Уменьшить количество товара на складе.
- Списать деньги с банковской карты клиента.
Эти операции происходят в двух разных системах, например, одна база данных управляет складом, а другая обрабатывает банковские операции. Как быть уверенными, что обе операции успешно завершились? Что если одна из них провалится? Вот тут и приходит на помощь двухфазный коммит.
Двухфазный коммит (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).
- Мониторить состояние транзакций и своевременно разбирать зависшие.
На этом мы завершаем погружение в двухфазный коммит. В следующей лекции мы обсудим, как оптимизировать транзакции в приложениях, не жертвуя производительностью.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ