← Назад к вопросам

Что такое хореография в Saga?

2.7 Senior🔥 121 комментариев
#Микросервисы и архитектура

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Хореография Saga Pattern

Хореография Saga — это подход к реализации длинных транзакций (Long Running Transactions) в распределенных системах, где каждый участник (сервис) самостоятельно принимает решения о продолжении или откате общей операции, основываясь на событиях, опубликованных другими участниками, без централизованного координатора.

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

Основные принципы

  • Децентрализация: отсутствует единый контролирующий компонент. Каждый сервис знает свою роль в бизнес-процессе.
  • Событийная модель: взаимодействие происходит через публикацию и потребление событий (обычно через Message Broker, например, Kafka, RabbitMQ).
  • Локальная автономия: каждый сервис самостоятельно определяет, как реагировать на полученное событие: выполнять свой основной шаг или компенсационное действие.
  • Идемпотентность: обработка событий должна быть идемпотентной, так как события могут быть повторно отправлены.

Пример реализации на Go

Рассмотрим упрощенный пример Saga "Оформление заказа", состоящей из трех сервисов: OrderService, PaymentService и InventoryService.

1. Определение событий и их обработчиков

// events.go
package saga

// Типы событий для хореографии Saga
const (
    EventOrderCreated   = "order.created"
    EventPaymentSuccess = "payment.success"
    EventPaymentFailed  = "payment.failed"
    EventStockReserved  = "stock.reserved"
    EventStockFailed    = "stock.failed"
    EventOrderCompleted = "order.completed"
    EventOrderCancelled = "order.cancelled"
)

// Структура события, передаваемого через брокер
type SagaEvent struct {
    Type      string                 // Тип события
    SagaID    string                 // Идентификатор Saga (например, ID заказа)
    Data      map[string]interface{} // Данные события (ID пользователя, сумма, etc.)
    Timestamp time.Time
}

2. Сервис заказа (OrderService)

Этот сервис начинает Saga, создавая первое событие.

// order_service.go
package order

import (
    "context"
    "saga"
)

type OrderService struct {
    eventPublisher EventPublisher // Интерфейс для публикации событий
}

func (s *OrderService) CreateOrder(ctx context.Context, orderReq OrderRequest) error {
    orderID := generateID()
    // 1. Сохраняем заказ в локальной БД в состоянии "PENDING"
    err := s.saveOrder(ctx, orderID, orderReq)
    if err != nil {
        return err
    }

    // 2. Публикуем событие начала Saga. Другие сервисы начинают свою работу.
    event := saga.SagaEvent{
        Type:   saga.EventOrderCreated,
        SagaID: orderID,
        Data: map[string]interface{}{
            "user_id": orderReq.UserID,
            "amount":  orderReq.TotalAmount,
            "items":   orderReq.Items,
        },
    }
    return s.eventPublisher.Publish(ctx, event)
}

// Обработчик компенсационного события. Сервис заказа слушает события отката.
func (s *OrderService) HandleOrderCancellation(ctx context.Context, event saga.SagaEvent) error {
    // Обновляем статус заказа на "CANCELLED" в локальной БД
    return s.updateOrderStatus(ctx, event.SagaID, "CANCELLED")
}

3. Сервис оплаты (PaymentService)

Этот сервис реагирует на событие order.created, выполняет свою работу и публикует результат.

// payment_service.go
package payment

type PaymentService struct {
    eventPublisher EventPublisher
    eventConsumer  EventConsumer // Для подписки на события
}

// Обработчик события создания заказа. Вызывается при получении EventOrderCreated.
func (s *PaymentService) ProcessPayment(ctx context.Context, event saga.SagaEvent) error {
    orderID := event.SagaID
    amount := event.Data["amount"].(float64)

    // 1. Пытаемся выполнить платеж
    err := s.chargeUser(ctx, event.Data["user_id"].(string), amount)
    if err != nil {
        // Если платеж не удался, публикуем событие FAILURE.
        compensationEvent := saga.SagaEvent{
            Type:   saga.EventPaymentFailed,
            SagaID: orderID,
            Data:   map[string]interface{}{"reason": err.Error()},
        }
        s.eventPublisher.Publish(ctx, compensationEvent)
        return nil // Локальная ошибка обработана, Saga продолжается через события
    }

    // 2. Если успешно, публикуем событие SUCCESS.
    successEvent := saga.SagaEvent{
        Type:   saga.EventPaymentSuccess,
        SagaID: orderID,
    }
    return s.eventPublisher.Publish(ctx, successEvent)
}

// Обработчик компенсационного события (например, EventStockFailed).
func (s *PaymentService) HandleCompensation(ctx context.Context, event saga.SagaEvent) error {
    // Если резервирование товара не удалось, нужно вернуть деньги.
    return s.refundPayment(ctx, event.SagaID)
}

4. Сервис инвентаря (InventoryService)

Этот сервис слушает событие успешной оплаты и только тогда резервирует товар.

// inventory_service.go
package inventory

type InventoryService struct {
    eventPublisher EventPublisher
    eventConsumer  EventConsumer
}

// Обработчик события успешной оплаты. Вызывается при получении EventPaymentSuccess.
func (s *InventoryService) ReserveStock(ctx context.Context, event saga.SagaEvent) error {
    orderID := event.SagaID
    items := event.Data["items"].([]Item)

    // 1. Резервируем товары на складе
    err := s.reserveItems(ctx, orderID, items)
    if err != nil {
        // Если резервирование не удалось, публикуем событие FAILURE.
        // Это компенсационное событие, которое запустит откат в PaymentService.
        failEvent := saga.SagaEvent{
            Type:   saga.EventStockFailed,
            SagaID: orderID,
            Data:   map[string]interface{}{"reason": err.Error()},
        }
        s.eventPublisher.Publish(ctx, failEvent)
        return nil
    }

    // 2. Если успешно, публикуем событие SUCCESS, которое завершит Saga.
    successEvent := saga.SagaEvent{
        Type:   saga.EventStockReserved,
        SagaID: orderID,
    }
    s.eventPublisher.Publish(ctx, successEvent)

    // Сервис заказа, слушая EventStockReserved, может завершить Saga, опубликовав EventOrderCompleted.
    return nil
}

Последовательность событий при успешном выполнении

OrderService          PaymentService          InventoryService
     |                       |                       |
CreateOrder()                |                       |
     |                       |                       |
Publish(OrderCreated) --> Consume(OrderCreated)      |
     |                 ProcessPayment()              |
     |                       |                       |
     |                 Publish(PaymentSuccess) --> Consume(PaymentSuccess)
     |                       |                 ReserveStock()
     |                       |                       |
     |                       |                 Publish(StockReserved)
Consume(StockReserved)       |                       |
MarkOrderCompleted()         |                       |

Последовательность событий при откате (Payment Failed)

OrderService          PaymentService          InventoryService
     |                       |                       |
CreateOrder()                |                       |
     |                       |                       |
Publish(OrderCreated) --> Consume(OrderCreated)      |
     |                 ProcessPayment()              |
     |                 (FAILURE)                     |
     |                 Publish(PaymentFailed) --> Consume(PaymentFailed)
Consume(PaymentFailed)       |                       |
MarkOrderCancelled()         |                       |

Преимущества и недостатки хореографии Saga

Преимущества:

  • Высокая степень декомпозиции и автономии сервисов.
  • Отсутствие точки отказа в виде координатора.
  • Более естественно ложится на событийно-ориентированную архитектуру (EDA).
  • Сервисы меньше знают о друг друге, только о типах событий.

Недостатки и сложности:

  • Трудность понимания полного потока Saga, так как он "размазан" по нескольким сервисам.
  • Сложность отладки и мониторинга. Требуется агрегировать события по SagaID.
  • Риск циклических зависимостей событий.
  • Проблема "порочного цикла" (cyclic rollback), если компенсационные действия тоже могут失败нуть, требуется тщательное проектирование.
  • Сложность внесения изменений в бизнес-процесс, нужно корректировать логику в нескольких независимых сервисах.

Практические рекомендации для Go

  • Используйте корпоративный шин событий (Event Bus) или надежный Message Broker для гарантированной доставки событий.
  • Внедрите систему отслеживания Saga (Saga Log). Все события должны сохраняться с привязкой к SagaID для возможности восстановления состояния и анализа.
  • Реализуйте механизм повторной обработки (retry) и таймауты для событий на уровне потребителей.
  • Тщательно тестируйте компенсационные сценарии.
  • Рассмотрите использование State Machines внутри каждого сервиса для более четкого управления локальными состояниями в контексте Saga.

Хореография Saga — это мощный, но сложный паттерн, который требует высокого уровня дисциплины в проектировании событийной модели и надежной инфраструктуры коммуникации между микросервисами.