Що таке патерн 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+ сервісів потрібна оркестрація.
- ❌ «Якщо впала компенсація, ми просто логуємо помилку і йдемо далі» — Компенсації зобов’язані повторюватися до переможного кінця або ескалуватися черговому інженеру.