Как реализуется паттерн Saga для координации транзакций между сервисами?

«Как реализуется паттерн Saga для координации транзакций между сервисами?» — вопрос из категории Архитектура, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Saga — это паттерн для управления последовательностью локальных транзакций в разных сервисах, которая в целом представляет собой бизнес-транзакцию. При сбое на каком-либо шаге выполняются компенсирующие транзакции (откаты) для предыдущих шагов.

Есть два основных стиля реализации:

1. Хореография (Choreography):

  • Принцип: Сервисы общаются через события (event-driven). Каждый сервис знает, на какие события реагировать и какие публиковать дальше.
  • Пример (Заказ товара):
    1. OrderService создает заказ в статусе Pending и публикует событие OrderCreated.
    2. PaymentService слушает OrderCreated, списывает деньги и публикует PaymentCompleted.
    3. InventoryService слушает PaymentCompleted, резервирует товар и публикует InventoryReserved.
    4. OrderService слушает InventoryReserved и меняет статус заказа на Completed.
  • Плюсы: Децентрализация, слабая связанность.
  • Минусы: Сложно отслеживать общий поток, циклические зависимости.

2. Оркестрация (Orchestration):

  • Принцип: Центральный координатор (оркестратор) управляет процессом, отправляя команды сервисам и обрабатывая их ответы.
  • Пример реализации с БД для отслеживания состояния:
    -- Таблица для хранения состояния каждой саги
    CREATE TABLE OrderSaga (
        SagaId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
        OrderId INT NOT NULL,
        CurrentStep INT NOT NULL,
        Status NVARCHAR(20) NOT NULL CHECK (Status IN ('Pending', 'Completed', 'Failed', 'Compensating')),
        SagaData NVARCHAR(MAX) -- JSON с данными для компенсации
    );
  • Логика оркестратора (псевдокод):

    // 1. Начать сагу, записать в БД
    var saga = new OrderSaga { OrderId = orderId, Status = "Pending" };
    _dbContext.Sagas.Add(saga);
    await _dbContext.SaveChangesAsync();
    
    // 2. Выполнить шаги последовательно
    try {
        await _paymentService.ChargeAsync(orderId);
        saga.CurrentStep = 1;
        await _inventoryService.ReserveAsync(orderId);
        saga.CurrentStep = 2;
        saga.Status = "Completed";
    }
    catch (Exception) {
        // 3. Запустить компенсацию в обратном порядке
        saga.Status = "Compensating";
        if (saga.CurrentStep >= 2) await _inventoryService.ReleaseAsync(orderId);
        if (saga.CurrentStep >= 1) await _paymentService.RefundAsync(orderId);
        saga.Status = "Failed";
    }
    finally {
        await _dbContext.SaveChangesAsync();
    }

Критически важные требования для Saga:

  • Идемпотентность: Каждый шаг и компенсация должны быть идемпотентными (повторный вызов с теми же данными не должен менять результат). Это защищает от повторной обработки сообщений.
  • Надежная доставка команд/событий: Достигается через паттерн Outbox (сохранение исходящего сообщения в БД в той же транзакции, что и изменение состояния) и фонового процесса-релея.

Инструменты: Для .NET можно использовать готовые фреймворки, такие как MassTransit или NServiceBus, которые предоставляют встроенную поддержку Saga с persistence-слоем.