Вопрос 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. В чём разница между синхронной и асинхронной коммуникацией]]