Як реалізувати розподілені транзакції в мікросервісах
У мікросервісах немає єдиної бази даних, тому звичайні транзакції (ACID) не працюють.
🟢 Junior Level
У мікросервісах немає єдиної бази даних, тому звичайні транзакції (ACID) не працюють.
Два підходи:
- Saga — послідовність локальних транзакцій з компенсаціями
- Two-Phase Commit (2PC) — протоколи розподілених транзакцій (рідко використовується)
Saga популярніша за 2PC, тому що не блокує ресурси на час усієї транзакції і працює з гетерогенними БД, тоді як 2PC вимагає підтримки двофазного комміту від усіх учасників.
Замовлення товару:
1. Order Service: створити замовлення (локальна транзакція)
2. Payment Service: списати гроші (локальна транзакція)
3. Inventory Service: зарезервувати товар (локальна транзакція)
Якщо крок 2 впав → скасувати замовлення (компенсація)
Мета: Eventual consistency — дані узгоджені, але не миттєво.
Коли НЕ реалізовувати розподілені транзакції
Якщо можна перепроектувати так, щоб один сервіс володів усіма даними — уникайте розподілених транзакцій повністю. Це найнадійніше рішення.
🟡 Middle Level
Реалізація Saga
1. Choreography (event-driven):
// Order Service
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
eventPublisher.publish(new OrderCreatedEvent(order.getId()));
}
// Payment Service
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
try {
paymentService.charge(event.orderId());
eventPublisher.publish(new PaymentCompletedEvent(event.orderId()));
} catch (Exception e) {
eventPublisher.publish(new PaymentFailedEvent(event.orderId(), e));
}
}
// Order Service — компенсація
@EventListener
public void onPaymentFailed(PaymentFailedEvent event) {
orderService.cancelOrder(event.orderId());
eventPublisher.publish(new OrderCancelledEvent(event.orderId()));
}
2. Orchestration (coordinator):
@Component
public class OrderOrchestrator {
public void processOrder(Order order) {
try {
orderService.create(order);
paymentService.charge(order);
inventoryService.reserve(order);
} catch (PaymentException e) {
orderService.cancel(order);
throw e;
} catch (InventoryException e) {
paymentService.refund(order);
orderService.cancel(order);
throw e;
}
}
}
Типові помилки
- Відсутність ідемпотентності:
Подія доставлена двічі → payment спишеться двічі! Рішення: перевіряйте orderID перед обробкою - Втрачені події:
Event publisher впав → подія не доставлена Рішення: transactional outbox pattern
🔴 Senior Level
Transactional Outbox Pattern
Outbox — спеціальна таблиця в тій же БД, куди в одній транзакції з бізнес-даними пишеться подія. CDC (Change Data Capture, Debezium) читає логи БД і відправляє події в Kafka.
// Проблема: як гарантувати доставку подій?
// Рішення: Outbox table в тій же транзакції
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
outboxRepository.save(new OutboxEvent(
"OrderCreated", order.getId(), payload
));
// Обидва зберігаються в одній транзакції!
}
// CDC (Change Data Capture) або polling читає outbox
// і публікує в Kafka
@Component
public class OutboxPublisher {
@Scheduled(fixedDelay = 1000)
public void publishPending() {
List<OutboxEvent> events = outboxRepository.findPending();
for (OutboxEvent event : events) {
kafkaTemplate.send(event.getTopic(), event.getPayload());
outboxRepository.markPublished(event.getId());
}
}
}
Архітектурні Trade-offs
| Підхід | Плюси | Мінуси |
|---|---|---|
| Saga | Гнучкий, масштабований | Складна компенсація |
| 2PC | Строга узгодженість | Блокування, повільний |
| Eventual | Простий | Немає гарантій |
Edge Cases
1. Concurrent Sagas:
Saga1: Order A → reserve inventory item1
Saga2: Order B → reserve inventory item1
Конкуренція → одне із замовлень провалиться
Рішення: optimistic/pessimistic locking
2. Saga timeout:
// Приклад ілюстративний (Axon Framework). Реальний API залежить від бібліотеки.
// Захист від "завислих" Saga
@SagaConfiguration
public class OrderSagaConfig {
@Bean
public SagaConfiguration<OrderSaga> orderSaga() {
return SagaConfiguration.sagaManagedBy(OrderSaga.class)
.sagaTimeout(Duration.ofMinutes(5));
}
}
Production Experience
Debezium CDC + Kafka:
Database → Debezium → Kafka → Event Consumers
Outbox table → Debezium CDC → Kafka topic → Saga handlers
Це гарантує:
- Доставка хоча б один раз (at-least-once)
- Порядок подій
- Відновлення після збоїв
Best Practices
✅ Використовуйте Transactional Outbox
✅ Реалізуйте idempotency на кожному кроці
✅ Додайте Saga timeout
✅ Логуйте кожен крок
✅ Моніторьте тривалість Saga
❌ Не використовуйте 2PC без необхідності
❌ Не ігноруйте concurrent Sagas
❌ Не забувайте про компенсації
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- Два основні підходи: Saga та Two-Phase Commit (2PC)
- Saga популярніша: не блокує ресурси, працює з різними БД
- Transactional Outbox гарантує доставку подій (write в одній транзакції з бізнес-даними)
- CDC (Debezium) читає outbox і публікує в Kafka
- Idempotency обов’язкова на кожному кроці
- Concurrent Sagas вимагають locking механізмів
- Saga timeout захищає від транзакцій, що зависли
- Найкращий підхід — уникнути розподілених транзакцій перепроектуванням
Часті уточнюючі питання:
- Чому Saga краща за 2PC? 2PC блокує ресурси на всю транзакцію і вимагає підтримки від усіх учасників. Saga — асинхронна і гнучка.
- Як гарантувати доставку подій? Transactional Outbox + CDC — запис події в тій же транзакції що й бізнес-дані.
- Що таке eventual consistency? Дані стануть узгодженими через час, але не миттєво.
- Як обробляти concurrent Sagas? Optimistic/pessimistic locking на рівні ресурсів.
Червоні прапорці (НЕ говорити):
- “2PC — найкраще рішення для мікросервісів” — ні, занадто повільний і блокуючий
- “Outbox не потрібен, Kafka гарантує доставку” — Kafka гарантує доставку, але не атомарність з БД
- “Eventual consistency = дані можуть бути невірними” — ні, вони стануть вірними
- “Розподілені транзакції не потрібні, можна все в одному сервісі” — не завжди можливо
Пов’язані теми:
- [[1. Що таке патерн Saga і коли його використовувати]]
- [[2. У чому різниця між хореографією та оркестрацією в Saga]]
- [[4. Що таке компенсувальні транзакції]]
- [[13. Що таке патерн Database per Service]]
- [[16. У чому різниця між синхронною та асинхронною комунікацією]]