Питання 3 · Розділ 17

Як реалізувати розподілені транзакції в мікросервісах

У мікросервісах немає єдиної бази даних, тому звичайні транзакції (ACID) не працюють.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

У мікросервісах немає єдиної бази даних, тому звичайні транзакції (ACID) не працюють.

Два підходи:

  1. Saga — послідовність локальних транзакцій з компенсаціями
  2. 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;
        }
    }
}

Типові помилки

  1. Відсутність ідемпотентності:
    Подія доставлена двічі → payment спишеться двічі!
    Рішення: перевіряйте orderID перед обробкою
    
  2. Втрачені події:
    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. У чому різниця між синхронною та асинхронною комунікацією]]