Ответ
Паттерн 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), чтобы восстановиться после сбоя.
- Тайм-ауты: Для контроля времени выполнения шагов и автоматического запуска компенсации.
Ответ 18+ 🔞
Давай разберём эту вашу сагу, блядь, как она работает, а то некоторые думают, что это про викингов сказка, ёпта.
Вот представь: у тебя микросервисы, каждый в своей берлоге сидит, и им надо вместе дело сделать — типа заказ оформить. А транзакции-то распределённые, классический 2PC не завезли, потому что все умные и говорят «это legacy, нахуй». И тут на сцену выходит паттерн Saga, как спаситель, блядь. А у него два лица, как у Януса, только оба ебаные.
1. Хореография (Choreography)
Это когда у тебя бардак полный, но якобы «слабая связанность». Представь танцплощадку в деревенском клубе: музыку включили, и все начинают дёргаться кто во что горазд. Никакого дирижёра, блядь.
- Как работает: Один сервис сделал своё дело и орёт в кафку: «Эй, пацаны, заказ создан!». Другой сервис, который за платежи отвечает, слышит этот крик, бежит делать свою работу. Если всё ок — орёт дальше: «Деньги спизжены!». Если нихуя не вышло — орёт: «Всё пропало, шеф, откатывайтесь!». И все остальные, услышав это, начинают судорожно откатывать то, что натворили.
- Плюсы: Нет главного начальника, все якобы независимые. Масштабируется — хоть сто сервисов заведи.
- Минусы: А попробуй потом понять, кто из этих долбоёбов на каком этапе обосрался. Логика раскидана по всем щелям, отладка — это просто пиздец и ад.
// Вот смотри, как сервис платежей может выглядеть. Просто слушает и орёт.
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)
А это уже не деревенская дискотека, а военный парад, ёпта. Есть один сервис-оркестратор — этакий главный по тарелочкам, который командует: «Ты — делай раз! Ты — делай два!».
- Как работает: Этот командир вызывает сервис А, ждёт, пока тот отчитается. Потом тычет пальцем в сервис Б: «Теперь твой черёд!». Если сервис Б вдруг падает и молчит, командир такой: «Ах ты ж сука! Ну-ка быстро, сервис А, отмени всё, что сделал!».
- Плюсы: Всё как на ладони, один чувак знает весь план. Ошибки отслеживать — раз плюнуть.
- Минусы: А если этого командира прибьёт? Вся система — в ахуе, потому что это единая точка отказа. И все от него сильно зависят, связанность — овердохуищная.
// Смотри, вот упрощённо этот командир может выглядеть. Прямо начальник цеха.
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 пихать. Чтобы после падения и перезапуска не начинать с чистого листа, а то будет «ядра — чистый изумруд», блядь.
- Тайм-ауты: А то какой-нибудь сервис зависнет и будет молчать как Герасим. Ждать его до второго пришествия? Не, нужно по таймауту понимать, что пора запускать компенсацию и всем разбегаться.
Вот и вся философия. Выбирай, что тебе ближе: анархия хореографии или тоталитаризм оркестрации. Оба варианта — веселье, просто разное.