Ответ
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-слоем.
Ответ 18+ 🔞
А, саги, блядь! Ну это ж классика, когда у тебя одна операция по бизнесу раскидана по куче сервисов, и надо, чтобы они все как-то согласованно отработали, а не накосячили. Представь, что ты заказываешь пиццу, пиво и чипсы в разных местах, но платишь одной картой. Если чипсы кончились, то и пиццу с пивом уже как-то не очень, и деньги назад вернуть надо. Вот сага примерно про это.
Так, есть два главных подхода, как эту кашу организовать:
1. Хореография (Chореография). Типа анархия, мать её. Каждый сервис сам за себя. Один что-то сделал — крикнул об этом всему миру (событие кинул). Другой услышал, сделал своё дело и тоже крикнул. И так по цепочке.
- Как это выглядит на примере заказа:
- Сервис заказов (
OrderService) создаёт заказ в состоянии «ждун» и орет:OrderCreated! - Сервис оплаты (
PaymentService) слышит этот крик, списывает бабки и орет:PaymentCompleted! - Сервис склада (
InventoryService) слышит про оплату, резервирует товар и орет:InventoryReserved! - Сервис заказов снова в деле, слышит, что товар зарезервирован, и переводит заказ в «готово».
- Сервис заказов (
- Плюсы: Все независимые, связанность слабая, сервисы не знают друг о друге напрямую.
- Минусы: А чёрт его знает, кто сейчас что делает! Поток событий проследить — голову сломаешь. И если логика сложная, можно накрутить таких циклических зависимостей, что сам потом будешь плакать.
2. Оркестрация. Тут уже появляется главный по тарелочкам — оркестратор. Он, как мама в семье, всем командует: ты сделай то, ты сделай это. И сам же помнит, кто что уже сделал.
- Оркестратору надо где-то состояние хранить. Обычно в БД.
-- Таблица, где оркестратор свои дела записывает 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); // Шаг 1: списать деньги saga.CurrentStep = 1; await _inventoryService.ReserveAsync(orderId); // Шаг 2: резервировать товар 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(); // Сохраняем итоговое состояние саги }
Но вот что самое, блядь, важное, без чего сага развалится как карточный домик:
- Идемпотентность. Это пиздец как важно! Каждый шаг и каждая компенсирующая операция должны быть идемпотентными. То есть если ты их вызовешь два раза с одними данными — результат должен быть как от одного вызова. А то представь, сообщение продублировалось, и оплату с тебя списали дважды. Или откат резервации товара выполнился два раза. Короче, пиздец, а не система.
- Надёжная доставка. Команды от оркестратора или события между сервисами должны доходить гарантированно. Для этого обычно используют паттерн Outbox — сначала записываешь исходящее сообщение в свою же базу (в той же транзакции, что и изменение состояния), а потом фоновый работяга (релей) его оттуда выгребает и гарантированно отправляет куда надо. Чтобы не было ситуации «деньги списались, а событие об этом потерялось».
Инструменты: Не городи велосипед, чувак. Для .NET есть готовые катки, типа MassTransit или NServiceBus. Они из коробки умеют и саги с состоянием хранить, и идемпотентность обрабатывать, и outbox паттерн реализовывать. Голыми руками это всё делать — тот ещё геморрой, ей-богу.