Что такое паттерн 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-транзакций:
- Каждый микросервис выполняет свою транзакцию в собственной базе данных и публикует событие.
- Следующий сервис слушает событие и запускает свою локальную транзакцию.
- Если на каком-то шаге происходит бизнес-ошибка (например, на складе не хватило товара), 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)!
Пока сага выполняется (а это может занимать от сотен миллисекунд до часов), промежуточные результаты локальных транзакций сразу же видны другим транзакциям. Это порождает классические аномалии параллелизма на уровне бизнес-логики:
- Грязное чтение (Dirty Read):
- Сага 1 списала деньги с баланса клиента.
- Параллельная Сага 2 видит уменьшенный баланс и отказывает в оформлении кредита.
- Сага 1 падает на шаге резервации склада и возвращает деньги. Сага 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 упадет посреди транзакции, состояние процесса будет потеряно, деньги списаны, а заказ зависнет.
Промышленное решение:
- Saga Execution Coordinator (SEC) сохраняет каждый шаг в реляционную таблицу
SagaLog(SagaId,CurrentStep,Payload,Status). - Команды микросервисам отправляются через паттерн Transactional Outbox, гарантируя, что запись в БД и отправка сообщения в брокер произойдут атомарно.
- При рестарте пода оркестратор вычитывает незавершенные саги из
SagaLogи возобновляет выполнение или компенсирует их по таймауту. - В реальном Enterprise-сегменте вместо самописных оркестраторов используют специализированные движки рабочих процессов: Temporal.io, Camunda (BPMN), Cadence или AWS Step Functions.
4 Tricky Questions
1. Какую из букв ACID нарушает паттерн Saga, и почему компенсирующая транзакция — это НЕ то же самое, что SQL ROLLBACK?
Ответ:
- Паттерн Saga нарушает букву
I(Isolation). Локальные транзакции каждого шага коммитятся в своих базах данных немедленно, делая промежуточное состояние видимым для всех параллельных пользователей системы. ROLLBACKв СУБД возвращает физическое состояние страниц данных в прошлое с помощью Undo-логов, как будто транзакции никогда не существовало.- Компенсирующая транзакция — это новая отдельная транзакция вперед (Compensating Forward Action). Она не стирает историю, а совершает семантически противоположное бизнес-действие (например, делает возврат средств
REFUND, создавая новую проводку в бухгалтерском журнале).
2. Что произойдет, если компенсирующая транзакция сама упадет с ошибкой (например, внешний платежный шлюз вернул HTTP 500)? Имеет ли право Saga остановиться?
Ответ:
- Нет, сага не имеет права останавливаться! Если бросить компенсацию на полпути, система останется в разорванном, неконсистентном состоянии (деньги списаны, а заказ отменен).
- Компенсирующие транзакции обязаны проектироваться абсолютно идемпотентными и повторяемыми (Idempotent & Highly Available).
- При ошибке компенсации оркестратор обязан выполнять бесконечные повторы с экспоненциальной задержкой (Exponential Backoff Retry).
- Если повторы исчерпаны или наступила фатальная ошибка (счет заблокирован), инцидент отправляется в Dead Letter Queue (DLQ) и формирует алерт для ручного вмешательства человека (Human-in-the-loop / Support Dashboard).
3. Как бороться с гонкой сообщений «компенсация пришла раньше прямого действия» (Out-of-order execution) в асинхронной Saga?
Ответ:
- В асинхронных сетях из-за сетевых задержек событие отмены
CancelPaymentможет обогнать задержавшееся сообщениеProcessPayment. - Если обработчик отмены выполнится первым, а затем придет запоздалое прямое списание денег, средства спишутся навсегда без компенсации.
- Решение — стейт-машина сущности:
- Когда сервис платежей получает
CancelPaymentдля еще неизвестногоpaymentId, он создает запись в статусеCANCELLED_BEFORE_ARRIVAL. - Когда позже приходит запоздалая команда
ProcessPayment, сервис видит статусCANCELLED_BEFORE_ARRIVALи не выполняет списание, отбрасывая команду как устаревшую.
- Когда сервис платежей получает
4. В чем фундаментальные отличия 2PC (Two-Phase Commit) от Saga, и почему 2PC практически не используется в современных микросервисах?
Ответ:
- Блокировки: 2PC на фазе Prepare физически удерживает блокировки строк во всех участвующих СУБД до получения команды Commit. В облаке с сотнями сервисов это приводит к блокировкам таблиц, экспоненциальному росту дедлоков и деградации производительности (задержка равна времени ответа самого медленного сервиса).
- Отказоустойчивость: Если координатор 2PC падает во время фазы фиксации, базы данных остаются заблокированными на неопределенный срок.
- Гетерогенность: 2PC требует поддержки стандарта XA всеми участниками. Современные брокеры (Kafka, RabbitMQ) и NoSQL СУБД (MongoDB, Cassandra) не поддерживают XA-транзакции.
- 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+ сервисов нужна оркестрация.
- ❌ «Если упала компенсация, мы просто логируем ошибку и идем дальше» — Компенсации обязаны повторяться до победного конца или эскалироваться дежурному инженеру.