Какие существуют способы координации транзакций в паттерне Saga?

Ответ

Паттерн Saga управляет распределенными транзакциями через последовательность локальных транзакций. Существует два основных способа координации:

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

Децентрализованный подход, где сервисы общаются друг с другом через асинхронные события (например, через Kafka или RabbitMQ). Каждый сервис подписывается на события, которые ему интересны, и публикует свои собственные события после выполнения своей части работы.

  • Принцип работы: Сервис A выполняет транзакцию и публикует событие OrderCreated. Сервис B слушает это событие, выполняет свою транзакцию и публикует PaymentProcessed. В случае ошибки сервис публикует событие сбоя, на которое реагируют другие сервисы для запуска компенсирующих транзакций.
  • Плюсы: Слабая связанность (loose coupling), высокая масштабируемость.
  • Минусы: Сложность отслеживания и отладки общего процесса, так как нет центральной точки управления.
// Пример обработчика событий в сервисе платежей
func handleOrderCreated(event OrderCreatedEvent) {
    if err := processPayment(event.OrderID); err != nil {
        // Публикуем событие сбоя для запуска компенсации
        publishEvent(PaymentFailedEvent{OrderID: event.OrderID, Reason: err.Error()})
    } else {
        publishEvent(PaymentSucceededEvent{OrderID: event.OrderID})
    }
}

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

Централизованный подход, где один сервис — оркестратор — управляет всем процессом. Он вызывает нужные сервисы в правильной последовательности и запускает компенсирующие транзакции в случае сбоя.

  • Принцип работы: Оркестратор вызывает сервис A, ждет ответа, затем вызывает сервис B. Если сервис B возвращает ошибку, оркестратор вызывает компенсирующую транзакцию для сервиса A.
  • Плюсы: Централизованная логика, проще отслеживать состояние, легче отлаживать и управлять ошибками.
  • Минусы: Оркестратор становится единой точкой отказа (SPOF) и может привести к сильной связанности.
// Упрощенный пример оркестратора
type SagaOrchestrator struct {
    steps []SagaStep // Шаги: {Execute: func(), Compensate: func()}
}

func (s *SagaOrchestrator) Execute() error {
    completedSteps := []SagaStep{}
    for _, step := range s.steps {
        if err := step.Execute(); err != nil {
            s.compensate(completedSteps) // Откатываем выполненные шаги
            return err
        }
        completedSteps = append(completedSteps, step)
    }
    return nil
}

func (s *SagaOrchestrator) compensate(stepsToCompensate []SagaStep) {
    for i := len(stepsToCompensate) - 1; i >= 0; i-- {
        stepsToCompensate[i].Compensate() // Вызов компенсирующих транзакций
    }
}

Ключевые аспекты реализации:

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