Ответ
Паттерн Saga — это способ управления согласованностью данных в распределенных системах без использования двухфазных коммитов (2PC). Сага состоит из последовательности локальных транзакций. Каждая транзакция обновляет данные в одном сервисе и публикует событие, которое запускает следующую транзакцию. Если шаг завершается неудачей, сага выполняет компенсирующие транзакции, которые отменяют предыдущие изменения.
Существует два основных подхода к реализации саги:
1. Оркестрация (Orchestration)
Центральный координатор (оркестратор) управляет всем процессом, вызывая сервисы в нужном порядке и запуская компенсирующие действия при сбое.
Пример простого оркестратора на Go:
package main
import "fmt"
// Step определяет шаг саги и его компенсацию.
type Step struct {
Name string
Execute func() error // Действие
Compensate func() error // Компенсирующее действие
}
// SagaOrchestrator управляет выполнением шагов.
type SagaOrchestrator struct {
steps []Step
}
// Run запускает сагу.
func (s *SagaOrchestrator) Run() error {
completedSteps := []Step{}
for _, step := range s.steps {
fmt.Printf("Выполнение шага: %sn", step.Name)
if err := step.Execute(); err != nil {
fmt.Printf("Ошибка на шаге '%s': %v. Запуск компенсации...n", step.Name, err)
s.compensate(completedSteps)
return err
}
completedSteps = append(completedSteps, step)
}
fmt.Println("Сага успешно завершена.")
return nil
}
// compensate выполняет откат в обратном порядке.
func (s *SagaOrchestrator) compensate(completed []Step) {
for i := len(completed) - 1; i >= 0; i-- {
step := completed[i]
fmt.Printf("Компенсация шага: %sn", step.Name)
if err := step.Compensate(); err != nil {
// В реальном приложении здесь нужна сложная логика:
// повторные попытки, логирование в dead-letter queue и т.д.
fmt.Printf("Критическая ошибка компенсации шага '%s': %vn", step.Name, err)
}
}
}
Плюсы: Централизованная логика, проще понять и отладить.
Минусы: Оркестратор становится единой точкой отказа и потенциальным узким местом.
2. Хореография (Choreography)
Децентрализованный подход, где каждый сервис после выполнения своей транзакции публикует событие в общую шину (например, Kafka, RabbitMQ). Другие сервисы подписываются на эти события и реагируют на них, запуская свои локальные транзакции. Компенсация также инициируется событиями.
Плюсы: Нет единой точки отказа, высокая масштабируемость.
Минусы: Сложно отслеживать состояние всего процесса, так как логика распределена по многим сервисам.
Важные аспекты для Production:
- Идемпотентность: Все шаги и компенсации должны быть идемпотентными, чтобы повторный вызов не приводил к некорректному состоянию.
- Сохранение состояния: Оркестратор должен сохранять свое состояние в базу данных, чтобы восстановить сагу после сбоя.
- Готовые решения: Для сложных сценариев лучше использовать готовые фреймворки, такие как Temporal или Cadence, которые предоставляют надежное выполнение, сохранение состояния, повторные попытки и видимость "из коробки". Также часто используется паттерн Outbox для атомарной записи в локальную БД и отправки события.