Ответ
Saga — это паттерн для управления последовательностью локальных транзакций в разных сервисах, которая в целом представляет собой бизнес-транзакцию. При сбое на каком-либо шаге выполняются компенсирующие транзакции (откаты) для предыдущих шагов.
Есть два основных стиля реализации:
1. Хореография (Choreography):
- Принцип: Сервисы общаются через события (event-driven). Каждый сервис знает, на какие события реагировать и какие публиковать дальше.
- Пример (Заказ товара):
OrderServiceсоздает заказ в статусеPendingи публикует событиеOrderCreated.PaymentServiceслушаетOrderCreated, списывает деньги и публикуетPaymentCompleted.InventoryServiceслушаетPaymentCompleted, резервирует товар и публикуетInventoryReserved.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-слоем.