Паттерн Saga

Saga в IT — это способ организовать одну бизнес-операцию, состоящую из нескольких локальных транзакций в разных сервисах, без общей распределённой транзакции.

Иными словами, Saga нужна, когда операция проходит через несколько микросервисов, у каждого своя база данных, а обычный BEGIN / COMMIT / ROLLBACK уже не может охватить всё сразу.

Пример проблемы

Допустим, пользователь покупает товар:

  1. Order Service создаёт заказ.
  2. Payment Service списывает деньги.
  3. Inventory Service резервирует товар.
  4. Delivery Service создаёт доставку.

У каждого сервиса собственная база:

Order DB
Payment DB
Inventory DB
Delivery DB

Одна SQL-транзакция между всеми этими базами невозможна или нежелательна.

Может произойти такое:

Заказ создан              успешно
Деньги списаны            успешно
Товар зарезервирован      успешно
Доставка создана          ошибка

Система оказалась в промежуточном состоянии. Деньги списаны, товар зарезервирован, но доставить его нельзя.

Saga определяет:

  1. последовательность локальных транзакций;
  2. действия, которые нужно выполнить при успехе;
  3. компенсирующие действия, которые нужно выполнить при ошибке.

Из чего состоит Saga

Saga выглядит как цепочка:

T1 → T2 → T3 → T4

Где каждая T — отдельная локальная транзакция.

Например:

T1: создать заказ
T2: списать деньги
T3: зарезервировать товар
T4: создать доставку

Для каждой операции обычно предусматривается компенсация:

C1: отменить заказ
C2: вернуть деньги
C3: снять резерв товара
C4: отменить доставку

Если ошибка возникла на четвёртом шаге, Saga запускает компенсации в обратном порядке:

T1 → T2 → T3 → T4 ✗

C3 → C2 → C1

Итог:

снять резерв товара
вернуть деньги
отменить заказ

Компенсация не равна rollback

Это важнейшее различение.

В обычной транзакции:

BEGIN;

UPDATE accounts
SET balance = balance - 100;

UPDATE orders
SET status = 'PAID';

ROLLBACK;

После ROLLBACK изменения будто бы вообще не происходили.

В Saga изменения уже могли быть:

  • зафиксированы в базе;
  • отправлены в Kafka;
  • показаны пользователю;
  • переданы внешнему банку;
  • отражены в бухгалтерском журнале.

Поэтому Saga не делает настоящий технический rollback всей системы.

Она выполняет новую бизнес-операцию, компенсирующую предыдущую.

Например:

Списание денег:
-100 долларов

Компенсация:
+100 долларов

Первая запись никуда не исчезает. Появляется вторая запись, которая компенсирует первую.

То есть:

rollback:
событие как будто не происходило

compensation:
событие произошло, а затем было компенсировано другим событием

Это особенно важно для денег, аудита, бухгалтерии и внешних систем.


Два основных способа реализации Saga

1. Choreography Saga

Choreography, или хореография, означает, что центрального управляющего сервиса нет.

Каждый сервис:

  1. слушает события;
  2. выполняет свою локальную транзакцию;
  3. публикует следующее событие.

Пример:

Order Service
  создаёт заказ
  публикует OrderCreated

Payment Service
  получает OrderCreated
  списывает деньги
  публикует PaymentCompleted

Inventory Service
  получает PaymentCompleted
  резервирует товар
  публикует InventoryReserved

Delivery Service
  получает InventoryReserved
  создаёт доставку
  публикует DeliveryCreated

Схема:

OrderCreated
      ↓
PaymentCompleted
      ↓
InventoryReserved
      ↓
DeliveryCreated

При ошибке публикуются события компенсации:

DeliveryCreationFailed
      ↓
ReleaseInventory
      ↓
RefundPayment
      ↓
CancelOrder

Преимущества

  • нет центрального координатора;
  • сервисы слабо связаны;
  • удобно для событийной архитектуры;
  • естественно работает с Kafka, RabbitMQ и другими брокерами.

Недостатки

По мере роста системы логика начинает размазываться между сервисами.

Например, чтобы понять весь процесс покупки, нужно изучить:

Order Service
Payment Service
Inventory Service
Delivery Service
Kafka topics
event handlers
retry policies
compensation handlers

Может возникнуть событийная паутина:

A публикует событие для B
B публикует событие для C
C при ошибке публикует событие для D
D публикует событие для A

Формально центрального управляющего элемента нет, но бизнес-процесс всё равно существует. Просто его форма распределена по обработчикам событий.


2. Orchestration Saga

Orchestration, или оркестрация, означает, что существует отдельный координатор, который управляет сценарием.

Например:

Order Saga Orchestrator

Он явно говорит сервисам, что делать:

1. Создать заказ
2. Списать деньги
3. Зарезервировать товар
4. Создать доставку

Схема:

                 ┌─────────────────┐
                 │ Saga Orchestrator│
                 └────────┬────────┘
                          │
          ┌───────────────┼───────────────┐
          ↓               ↓               ↓
      Payment         Inventory        Delivery
      Service          Service          Service

Упрощённая логика:

createOrder();

try {
    chargePayment();
    reserveInventory();
    createDelivery();
    completeOrder();
} catch (Exception error) {
    cancelDeliveryIfCreated();
    releaseInventoryIfReserved();
    refundPaymentIfCharged();
    cancelOrder();
}

В реальной системе это не обычный синхронный try/catch, потому что Saga может выполняться минуты, часы или дни. Состояние процесса сохраняется в базе.

Например:

Saga ID: saga-123
Order ID: order-456
State: INVENTORY_RESERVED
Payment: COMPLETED
Inventory: RESERVED
Delivery: PENDING

Преимущества

  • вся последовательность видна в одном месте;
  • легче понимать процесс;
  • проще диагностировать зависшие операции;
  • проще реализовать timeout, retry и компенсации;
  • хорошо подходит для сложных бизнес-процессов.

Недостатки

  • появляется центральный управляющий компонент;
  • оркестратор может стать слишком большим;
  • существует риск превратить его в «бог-сервис», знающий всё обо всех;
  • сервисы начинают зависеть от команд оркестратора.

Choreography и orchestration в сравнении

Характеристика Choreography Orchestration
Центральный координатор Нет Есть
Взаимодействие Через события Через команды и ответы
Где находится бизнес-процесс Распределён по сервисам Явно описан в оркестраторе
Простые процессы Удобно Иногда избыточно
Сложные процессы Может стать хаотично Обычно понятнее
Связанность Событийная Через протокол оркестрации
Наблюдаемость Сложнее Обычно проще

Пример Saga для банковского перевода

Возьмём перевод между двумя банками.

Условно:

Банк A → Клиринговая сеть → Банк B

Шаги Saga:

T1: проверить возможность перевода
T2: зарезервировать деньги в банке A
T3: отправить перевод в клиринговую сеть
T4: получить подтверждение банка B
T5: зачислить деньги в банке B
T6: окончательно списать резерв в банке A

Если банк B отклонил перевод:

C3: отменить платёжное поручение, если это ещё возможно
C2: снять резерв в банке A
C1: отметить перевод как отменённый

Но здесь появляется важный предел компенсации.

Допустим, деньги уже зачислены получателю, а получатель их потратил. Простого «отката» может уже не существовать.

Тогда компенсация превращается в отдельный бизнес-процесс:

запрос возврата
оспаривание платежа
создание обратного перевода
ручная проверка
взыскание

Поэтому не каждая транзакция полностью обратима.


Типы шагов Saga

В Saga полезно различать несколько видов транзакций.

1. Compensatable transaction

Операция, которую можно компенсировать.

Пример:

зарезервировать товар

Компенсация:

снять резерв

2. Pivot transaction

Поворотная транзакция, после которой процесс становится фактически необратимым или существенно труднее обратимым.

Пример:

отправить банковский перевод во внешнюю сеть

До этого шага можно было просто отменить внутренний резерв. После этого деньги уже вышли за пределы системы.


3. Retrievable transaction

Операция, которую после pivot-шагa нужно не компенсировать, а повторять до успешного завершения.

Например:

банк-получатель уже подтвердил перевод,
но локальная запись статуса не обновилась

Нельзя отменить весь перевод только потому, что локальная база временно недоступна. Нужно повторять обновление до успеха.


Что Saga гарантирует

Saga позволяет обеспечить бизнес-согласованность, но не создаёт мгновенную глобальную атомарность.

В течение некоторого времени система может выглядеть так:

Payment Service: PAID
Inventory Service: NOT_RESERVED
Order Service: PAYMENT_PROCESSING

Это промежуточное состояние.

Через некоторое время система придёт либо к:

COMPLETED

либо к:

COMPENSATED

Такую модель часто связывают с eventual consistency, то есть согласованностью, достигаемой со временем.

Но Saga сама по себе не является магическим механизмом eventual consistency. Она является явным протоколом движения через промежуточные состояния и компенсации.


Основные сложности Saga

1. Идемпотентность

Любой шаг может быть выполнен повторно.

Например, оркестратор отправил:

ChargePayment(order-123)

Payment Service списал деньги, но ответ потерялся.

Оркестратор не знает, был ли платёж выполнен, и повторяет команду.

Без идемпотентности получится:

первое списание: -100
повторное списание: -100

Поэтому команда должна содержать уникальный ключ:

operationId = payment-order-123

Payment Service хранит результат:

operation_id        status       result
payment-order-123   COMPLETED    payment-789

При повторном запросе сервис не списывает деньги ещё раз, а возвращает прежний результат.


2. Дублирование сообщений

Брокер сообщений обычно гарантирует доставку в режиме:

at least once

То есть сообщение будет доставлено как минимум один раз, но может быть доставлено несколько раз.

Следовательно, обработчики должны допускать:

OrderCreated
OrderCreated
OrderCreated

без тройного списания денег.


3. Потеря события между базой и брокером

Пример:

1. Сервис обновил базу.
2. Сервис должен опубликовать событие.
3. Сервис упал до публикации.

Получается:

заказ создан,
но OrderCreated не отправлен

Для решения часто используют Transactional Outbox.

В одной локальной транзакции сервис записывает:

Order
OutboxEvent

Пример:

BEGIN;

INSERT INTO orders (...);

INSERT INTO outbox_events (
    event_id,
    event_type,
    payload
) VALUES (
    'event-123',
    'OrderCreated',
    '{...}'
);

COMMIT;

Затем отдельный процесс читает outbox_events и публикует события в Kafka.


4. Порядок сообщений

Может произойти:

PaymentCompleted
PaymentCancelled

Но потребитель получит:

PaymentCancelled
PaymentCompleted

Поэтому нужно учитывать:

  • версию агрегата;
  • номер последовательности;
  • ключ партиционирования;
  • допустимые переходы состояния.

Например:

version 1: PAYMENT_STARTED
version 2: PAYMENT_COMPLETED
version 3: PAYMENT_REFUNDED

Событие версии 2 не должно перезаписать уже применённое состояние версии 3.


5. Timeout

Сервис может не ответить.

Это не обязательно означает, что операция не выполнена.

Например:

Payment Service списал деньги,
но сеть оборвалась до ответа.

Timeout означает только:

ответ не был получен за установленное время

Он не означает:

операция точно не произошла

Поэтому после timeout система должна:

  • проверить статус по operationId;
  • безопасно повторить идемпотентную команду;
  • перевести Saga в состояние ожидания;
  • отправить операцию на ручную проверку.

6. Компенсация тоже может завершиться ошибкой

Например:

списание прошло успешно;
доставка не создалась;
возврат денег временно не проходит.

Saga не может просто сказать: «rollback не удался».

Она должна хранить состояние:

COMPENSATION_PENDING

И повторять компенсацию:

REFUND_RETRY_1
REFUND_RETRY_2
REFUND_RETRY_3

После исчерпания автоматических попыток:

MANUAL_REVIEW_REQUIRED

Saga как конечный автомат

На практике Saga удобно моделировать не как линейный метод, а как state machine, конечный автомат.

Например:

CREATED
  ↓
PAYMENT_PENDING
  ↓
PAYMENT_COMPLETED
  ↓
INVENTORY_PENDING
  ↓
INVENTORY_RESERVED
  ↓
DELIVERY_PENDING
  ↓
COMPLETED

Ветка ошибки:

INVENTORY_FAILED
  ↓
PAYMENT_REFUND_PENDING
  ↓
PAYMENT_REFUNDED
  ↓
CANCELLED

Каждый переход должен быть явно разрешён:

PAYMENT_PENDING → PAYMENT_COMPLETED
PAYMENT_PENDING → PAYMENT_FAILED
PAYMENT_COMPLETED → PAYMENT_REFUND_PENDING

Но нельзя случайно сделать:

CANCELLED → PAYMENT_COMPLETED

Когда Saga действительно нужна

Saga обычно применяется, когда:

  • операция затрагивает несколько сервисов;
  • у сервисов отдельные базы данных;
  • глобальная ACID-транзакция невозможна;
  • процесс может выполняться долго;
  • промежуточные состояния допустимы;
  • для выполненных действий существуют компенсации или процедуры завершения.

Примеры:

  • оформление заказа;
  • бронирование путешествия;
  • банковский перевод;
  • страховая выплата;
  • подключение клиента к услуге;
  • создание подписки;
  • резервирование билетов;
  • многоэтапная регистрация клиента.

Когда Saga может быть лишней

Не нужно вводить Saga только потому, что система названа микросервисной.

Если несколько изменений принадлежат одному бизнес-инварианту и всегда должны быть атомарными, возможно, их ошибочно разделили между сервисами.

Например, если для каждого изменения заказа нужно синхронно менять три сервиса, стоит проверить, не разрезан ли один домен искусственно.

Иногда лучше:

один сервис
одна база
одна транзакция

чем:

пять микросервисов
Kafka
Saga
Outbox
идемпотентность
компенсации
distributed tracing
ручное восстановление

Saga не устраняет сложность. Она делает распределённую сложность явной и управляемой.


Короткая формула

Saga =
последовательность локальных транзакций
+
сохранённое состояние процесса
+
компенсирующие действия
+
идемпотентность
+
повторы
+
обработка промежуточных состояний

Главное:

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

Прокрутить вверх