🏗️ Раздел 17 · Вопрос #1

Что такое паттерн Saga и когда его использовать

В монолитном приложении все таблицы лежат в одной СУБД. При оформлении заказа Spring выполняет единую транзакцию:

Паттерн Saga (Сага) — это архитектурный паттерн проектирования, предназначенный для управления распределенными транзакциями в микросервисных системах с изолированными базами данных (Database per Service). Поскольку классические распределенные транзакции на базе двухфазного коммита (2PC / XA) блокируют ресурсы и плохо масштабируются в распределенных средах, Saga заменяет их последовательностью независимых локальных транзакций, обеспечивая согласованность в конечном счете (Eventual Consistency) и откат через компенсирующие транзакции.


🟢 Junior Level

Почему в микросервисах не работает классический ACID

В монолитном приложении все таблицы лежат в одной СУБД. При оформлении заказа Spring выполняет единую транзакцию:

// Монолит: единая ACID-транзакция в одной БД
@Transactional
public void createOrder() {
    orderRepository.save(order);       // Шаг 1
    paymentService.charge(payment);    // Шаг 2
    inventoryService.reserve(items);   // Шаг 3
    // Ошибка на шаге 3? СУБД делает моментальный ROLLBACK всех таблиц!
}

В микросервисной архитектуре у каждого сервиса своя изолированная база данных (OrderDB, PaymentDB, InventoryDB). Единого менеджера транзакций нет, и обычный SQL ROLLBACK не может отменить изменения в чужой базе данных по сети.

                    ПРОБЛЕМА РАСПРЕДЕЛЕННОЙ ТРАНЗАКЦИИ
                    
  [ Order Service ]     ──>  [ Payment Service ]   ──>  [ Inventory Service ]
          │                          │                           │
          ▼                          ▼                           ▼
    [ Order DB ]              [ Payment DB ]              [ Inventory DB ]
  (Заказ создан)              (Деньги списаны)            (Товара нет! ОШИБКА!)
        │                          │                           │
        └──────────────────────────┴───────────────────────────┘
         КАК ОТМЕНИТЬ СПИСАНИЕ ДЕНЕГ В ЧУЖОЙ БАЗЕ ДАННЫХ?!

Как Saga решает проблему

Saga разбивает одну большую распределенную транзакцию на цепочку локальных ACID-транзакций:

  1. Каждый микросервис выполняет свою транзакцию в собственной базе данных и публикует событие.
  2. Следующий сервис слушает событие и запускает свою локальную транзакцию.
  3. Если на каком-то шаге происходит бизнес-ошибка (например, на складе не хватило товара), Saga запускает цепочку компенсирующих транзакций (Compensating Transactions) в обратном порядке, которые семантически нейтрализуют ранее сделанные изменения (возвращают деньги, отменяют заказ).
                            УСПЕШНЫЙ СЦЕНАРИЙ SAGA:
   [Создать заказ] ──> [Списать деньги] ──> [Зарезервировать товар] ──> [Успех]

                           СЦЕНАРИЙ С КОМПЕНСАЦИЕЙ:
   [Создать заказ] ──> [Списать деньги] ──> [Нет товара! СБОЙ!]
          │                    │                     │
          ▼                    ▼                     ▼
   [Отменить заказ] <── [Вернуть деньги] <───────────┘
     (Компенсация 2)      (Компенсация 1)

🟡 Middle Level

Два подхода к реализации Saga: Хореография vs Оркестрация

Критерий Choreography (Хореография) Orchestration (Оркестрация)
Координация Децентрализованная (Event-Driven через Kafka/RabbitMQ) Централизованная через выделенный сервис-оркестратор
Связанность (Coupling) Слабая (сервисы общаются только через топики событий) Сервисы знают команды оркестратора
Сложность понимания Высокая при > 4 сервисах (логика размазана по коду) Низкая (весь сценарий виден в коде одного координатора)
Циклические зависимости Высокий риск зацикливания событий Исключены
Точка отказа (SPOF) Отсутствует Оркестратор должен быть отказоустойчивым
Когда применять Простые цепочки из 2–4 сервисов Сложные бизнес-процессы (от 4–5 сервисов)

1. Хореография (Choreography):

Сервисы реагируют на события друг друга без единого центра:

  • OrderService сохраняет заказ со статусом PENDING и бросает событие OrderCreated.
  • PaymentService слушает OrderCreated, списывает баланс и бросает PaymentProcessed.
  • InventoryService слушает PaymentProcessed, обнаруживает нехватку и бросает InventoryFailed.
  • PaymentService слушает InventoryFailed и делает возврат средств (PaymentRefunded).
  • OrderService слушает PaymentRefunded и меняет статус заказа на REJECTED.

2. Оркестрация (Orchestration):

Выделенный компонент (Saga Orchestrator / State Machine) пошагово отправляет сервисам явные команды:

// Оркестратор управляет стейт-машиной процесса
public class OrderSagaOrchestrator {

    public void execute(CreateOrderCommand cmd) {
        orderService.createPendingOrder(cmd.orderId());
        
        PaymentResult payment = paymentService.authorize(cmd.orderId(), cmd.amount());
        if (!payment.isSuccess()) {
            orderService.cancelOrder(cmd.orderId());
            return;
        }

        InventoryResult inventory = inventoryService.reserve(cmd.orderId(), cmd.items());
        if (!inventory.isSuccess()) {
            // Запуск компенсирующих транзакций:
            paymentService.refund(cmd.orderId(), cmd.amount());
            orderService.cancelOrder(cmd.orderId());
            return;
        }

        orderService.approveOrder(cmd.orderId());
    }
}

🔴 Senior Level

Аномалии параллелизма из-за отсутствия буквы I (Isolation) в Saga

Классические распределенные транзакции обеспечивают ACID. Saga гарантирует только ACD (Atomicity, Consistency, Durability), но НЕ гарантирует изоляцию (Isolation = None)!

Пока сага выполняется (а это может занимать от сотен миллисекунд до часов), промежуточные результаты локальных транзакций сразу же видны другим транзакциям. Это порождает классические аномалии параллелизма на уровне бизнес-логики:

  1. Грязное чтение (Dirty Read):
    • Сага 1 списала деньги с баланса клиента.
    • Параллельная Сага 2 видит уменьшенный баланс и отказывает в оформлении кредита.
    • Сага 1 падает на шаге резервации склада и возвращает деньги. Сага 2 приняла бизнес-решение на основе промежуточных «грязных» данных!
  2. Потерянные обновления (Lost Update):
    • Сага A уменьшила остаток товара на складе с 10 до 8.
    • Сага B прочитала 8 и уменьшила до 5.
    • Сага A упала и компенсировала свое изменение, прибавив 2 (стало 7 вместо реальных 8).

Контрмеры изоляции (Saga Isolation Countermeasures):

  • Семантическая блокировка (Semantic Lock): Помечать записи промежуточным статусом (например, order.status = PENDING_APPROVAL, account.frozen_amount = 500). Другие транзакции блокируются бизнес-логикой до снятия флага.
  • Коммутативные обновления (Commutative Updates): Проектировать операции так, чтобы порядок их выполнения не влиял на результат (account.debit(50) и account.credit(50) коммутативны).
  • Пессимистичный взгляд (Pessimistic View): Переупорядочивать шаги саги так, чтобы операции с наибольшим финансовым риском выполнялись в самом конце.

Персистентный журнал саги (Saga Log) и Transactional Outbox

Оркестратор не может хранить состояние саги просто в оперативной памяти JVM. Если под Kubernetes упадет посреди транзакции, состояние процесса будет потеряно, деньги списаны, а заказ зависнет.

Промышленное решение:

  1. Saga Execution Coordinator (SEC) сохраняет каждый шаг в реляционную таблицу SagaLog (SagaId, CurrentStep, Payload, Status).
  2. Команды микросервисам отправляются через паттерн Transactional Outbox, гарантируя, что запись в БД и отправка сообщения в брокер произойдут атомарно.
  3. При рестарте пода оркестратор вычитывает незавершенные саги из SagaLog и возобновляет выполнение или компенсирует их по таймауту.
  4. В реальном Enterprise-сегменте вместо самописных оркестраторов используют специализированные движки рабочих процессов: Temporal.io, Camunda (BPMN), Cadence или AWS Step Functions.

4 Tricky Questions

1. Какую из букв ACID нарушает паттерн Saga, и почему компенсирующая транзакция — это НЕ то же самое, что SQL ROLLBACK?

Ответ:

  1. Паттерн Saga нарушает букву I (Isolation). Локальные транзакции каждого шага коммитятся в своих базах данных немедленно, делая промежуточное состояние видимым для всех параллельных пользователей системы.
  2. ROLLBACK в СУБД возвращает физическое состояние страниц данных в прошлое с помощью Undo-логов, как будто транзакции никогда не существовало.
  3. Компенсирующая транзакция — это новая отдельная транзакция вперед (Compensating Forward Action). Она не стирает историю, а совершает семантически противоположное бизнес-действие (например, делает возврат средств REFUND, создавая новую проводку в бухгалтерском журнале).

2. Что произойдет, если компенсирующая транзакция сама упадет с ошибкой (например, внешний платежный шлюз вернул HTTP 500)? Имеет ли право Saga остановиться?

Ответ:

  1. Нет, сага не имеет права останавливаться! Если бросить компенсацию на полпути, система останется в разорванном, неконсистентном состоянии (деньги списаны, а заказ отменен).
  2. Компенсирующие транзакции обязаны проектироваться абсолютно идемпотентными и повторяемыми (Idempotent & Highly Available).
  3. При ошибке компенсации оркестратор обязан выполнять бесконечные повторы с экспоненциальной задержкой (Exponential Backoff Retry).
  4. Если повторы исчерпаны или наступила фатальная ошибка (счет заблокирован), инцидент отправляется в Dead Letter Queue (DLQ) и формирует алерт для ручного вмешательства человека (Human-in-the-loop / Support Dashboard).

3. Как бороться с гонкой сообщений «компенсация пришла раньше прямого действия» (Out-of-order execution) в асинхронной Saga?

Ответ:

  1. В асинхронных сетях из-за сетевых задержек событие отмены CancelPayment может обогнать задержавшееся сообщение ProcessPayment.
  2. Если обработчик отмены выполнится первым, а затем придет запоздалое прямое списание денег, средства спишутся навсегда без компенсации.
  3. Решение — стейт-машина сущности:
    • Когда сервис платежей получает CancelPayment для еще неизвестного paymentId, он создает запись в статусе CANCELLED_BEFORE_ARRIVAL.
    • Когда позже приходит запоздалая команда ProcessPayment, сервис видит статус CANCELLED_BEFORE_ARRIVAL и не выполняет списание, отбрасывая команду как устаревшую.

4. В чем фундаментальные отличия 2PC (Two-Phase Commit) от Saga, и почему 2PC практически не используется в современных микросервисах?

Ответ:

  1. Блокировки: 2PC на фазе Prepare физически удерживает блокировки строк во всех участвующих СУБД до получения команды Commit. В облаке с сотнями сервисов это приводит к блокировкам таблиц, экспоненциальному росту дедлоков и деградации производительности (задержка равна времени ответа самого медленного сервиса).
  2. Отказоустойчивость: Если координатор 2PC падает во время фазы фиксации, базы данных остаются заблокированными на неопределенный срок.
  3. Гетерогенность: 2PC требует поддержки стандарта XA всеми участниками. Современные брокеры (Kafka, RabbitMQ) и NoSQL СУБД (MongoDB, Cassandra) не поддерживают XA-транзакции.
  4. Saga использует неблокирующие локальные транзакции и отлично масштабируется в асинхронных облачных архитектурах.

🎯 Шпаргалка для интервью

30-секундный ответ (Elevator Pitch)

«Паттерн Saga управляет распределенными транзакциями в микросервисах без использования блокирующего двухфазного коммита (2PC). Saga разбивает глобальную операцию на цепочку локальных транзакций в каждом сервисе. При возникновении бизнес-ошибки запускаются компенсирующие транзакции в обратном порядке для возврата системы к согласованному состоянию (Eventual Consistency). Существует два подхода: хореография (сервисы общаются через события в Kafka — для простых цепочек) и оркестрация (выделенный координатор или движок Temporal/Camunda управляет шагами — для сложных процессов). Главная особенность саги — отсутствие изоляции (ACD вместо ACID), что требует семантических блокировок и обязательной идемпотентности компенсаций».

Ключевые метрики для Production-мониторинга

  • Saga Completion Rate vs Compensation Rate: процент саг, завершившихся откатом (рост выше 5% указывает на проблемы со складом или платежами).
  • Saga Duration (End-to-End Latency): общее время выполнения всех шагов саги.
  • Dead Letter Queue (DLQ) / Failed Compensations Count: критический счетчик зависших компенсаций, требующих ручного вмешательства.

Красные флаги (Чего говорить НЕЛЬЗЯ)

  • ❌ «Saga гарантирует полную ACID-изоляцию транзакций» — В сагах нет изоляции (I), промежуточные изменения видны всем до завершения процесса.
  • ❌ «Компенсирующая транзакция откатывает базу данных назад как ROLLBACK» — Компенсация не откатывает БД, а выполняет новую прямую корректирующую транзакцию (REFUND).
  • ❌ «Хореография идеально подходит для сложных процессов из 15 сервисов» — Приведет к неконтролируемому клубку событий (Spaghetti Architecture), для 5+ сервисов нужна оркестрация.
  • ❌ «Если упала компенсация, мы просто логируем ошибку и идем дальше» — Компенсации обязаны повторяться до победного конца или эскалироваться дежурному инженеру.

Связанные темы