Saga: распределённые транзакции в микросервисах — choreography vs orchestration
Saga — это не магия. И не очередной фреймворк, который «сам всё сделает». Это стратегия: как провести бизнес-операцию через несколько микросервисов и не оставить данные в противоречивом состоянии, когда (не «если») что-то пойдёт не так.
В монолите всё просто: ACID-транзакция внутри одной базы данных — и если на третьем шаге из пяти случилась ошибка, ROLLBACK откатывает всё назад. Красиво. В микросервисах каждая база данных живёт своей жизнью. Заказ создан в Order DB, деньги списаны в Payment DB, а на складе — отказ. Поздравляю: у вас заказ без товара, деньги у клиента, и тикет с тегом «critical» в Jira.
Вот тут на сцену и выходит Saga.
Зачем нужна Saga: проблема распределённых транзакций
Классическая ACID-транзакция опирается на один экземпляр базы данных и один координатор транзакций. Всё или ничего. В микросервисной архитектуре такой роскоши нет: каждый сервис владеет своими данными и своей базой.
Распределённая транзакция — бизнес-операция, которая изменяет данные в нескольких независимых сервисах и должна быть выполнена атомарно: либо все изменения фиксируются, либо ни одно.
Первое, что приходит в голову — двухфазный коммит (2PC). Да, он существует. Да, он даже работает. Но 2PC блокирует ресурсы на время всей транзакции: если Payment Service отвечает 15 секунд, Order Service висит с открытой транзакцией и блокировками. В распределённой системе с десятками сервисов и тысячами запросов в секунду это не масштабируется. Совсем.
Saga решает проблему иначе: вместо одной атомарной транзакции — цепочка локальных транзакций в каждом сервисе. Если один из шагов падает, запускаются компенсирующие транзакции — действия, которые семантически отменяют уже выполненные шаги.
Два подхода: Choreography vs Orchestration
Паттерн Saga реализуется двумя принципиально разными способами. Оба решают одну задачу, но устроены по-разному, как ручная коробка передач и автомат.
Choreography (хореография)
При хореографии нет центрального координатора. Сервисы обмениваются событиями напрямую: один публикует событие — другие, кто на него подписан, реагируют.
На диаграмме видно: каждый сервис знает только о своём соседе по цепочке. Order Service публикует OrderCreated, Payment Service на него реагирует, публикует PaymentCompleted — и так до финального статуса. Никто не управляет процессом сверху.
Плюсы — низкая связанность и простота добавления новых сервисов. Захотелось отправлять push-уведомление после оплаты? Подписались на PaymentCompleted — готово, никто никого не трогал. Минусы — понять, на каком шаге застрял заказ, можно только проследив цепочку событий (и молиться, что они не потерялись по дороге).
Orchestration (оркестрация)
При оркестрации появляется Saga Orchestrator — отдельный компонент, который явно управляет последовательностью шагов. Он говорит каждому сервису: «создай заказ», «спиши деньги», «забронируй товар». И он же принимает решение об откате.
Цвета на диаграмме отражают фазы: зелёный — успешное выполнение шага, красный — сбой, жёлтый — компенсирующие действия. Orchestrator — мозг операции. Он хранит состояние саги (какие шаги выполнены, что пошло не так) и принимает решения.
Оркестрация заметно упрощает мониторинг: состояние саги лежит в одном месте, вам не нужно восстанавливать его, обходя события. Но Orchestrator становится единой точкой отказа (и умным местом, которое все забывают покрыть тестами).
Компенсирующие транзакции: как откатить изменения
В классическом ACID откат — это ROLLBACK. В Saga откат — это бизнес-логика. Если Payment Service списал деньги, а склад ответил «нет товара», нельзя просто сделать ROLLBACK в Payment DB: Payment Service — отдельный сервис, у него своя база (и он уже успел отправить клиенту «спасибо за покупку»).
Вместо этого выполняется компенсирующая транзакция — бизнес-операция, которая семантически отменяет предыдущий шаг. Списание денег компенсируется возвратом (RefundPayment). Создание заказа — отменой (CancelOrder). Важный нюанс: компенсация не обязана возвращать систему в точно то же состояние, что было до саги. В audit-логе останется запись «платёж создан», и это правильно.
Проектировать компенсации нужно сразу, на этапе описания каждого шага саги. Таблица ниже — минимальный шаблон:
| Шаг | Сервис | Действие | Компенсация если FAIL |
|---|---|---|---|
| 1 | Order Service | CreateOrder | CancelOrder |
| 2 | Payment Service | ProcessPayment | RefundPayment |
| 3 | Inventory Service | ReserveItems | ReleaseItems |
| 4 | Shipping Service | ScheduleShipment | CancelShipment |
Outbox Pattern: надёжная доставка событий
Проблема, о которой часто забывают: Saga опирается на сообщения и события. Если сервис обновил свою базу данных, но не смог отправить событие в брокер — сага «зависает». Order Service создал заказ и молчит. Payment Service ждёт. Клиент ждёт. Все ждут.
Outbox Pattern решает это атомарной записью: в той же транзакции, где сервис меняет свои данные, он пишет исходящее событие в таблицу outbox. Отдельный процесс (Change Data Capture или фоновый воркер) читает эту таблицу и публикует события в брокер сообщений.
Так вы гарантируете: либо и изменение данных, и событие записаны, либо ни того ни другого. Атомарность на уровне одного сервиса, которую нельзя обеспечить на уровне всей системы.
Практический пример: заказ в e-commerce
Представьте интернет-магазин. Пользователь кладёт товар в корзину и нажимает «Оплатить». За кулисами запускается сага из четырёх сервисов:
- Order Service создаёт заказ со статусом
PENDING. - Payment Service резервирует (холдирует) 5000 рублей на карте.
- Inventory Service резервирует товар на складе — и тут
OutOfStockException. - Saga Orchestrator запускает откат: возврат холда в Payment Service, отмена заказа в Order Service.
Что важно: статус заказа прошёл путь PENDING → PAYMENT_RESERVED → CANCELLED. Пользователь увидел «заказ отменён, средства вернутся в течение 3 дней». В идеальном мире всё отработало атомарно — никаких потерянных денег, никаких висящих резервов на складе. В реальном — ещё и написали логи на каждом шаге, чтобы понять, почему склад ответил отказом.
Здесь прослеживается прямая связь с идеями событийной архитектуры: сага — это, по сути, бизнес-процесс, собранный поверх событий. Разница лишь в том, кто координирует переходы.
Choreography vs Orchestration: таблица сравнения
| Критерий | Choreography | Orchestration |
|---|---|---|
| Связанность | Слабая (сервисы знают только события) | Средняя (сервисы знают Orchestrator) |
| Мониторинг | Сложный (нужно восстановить цепочку событий) | Простой (состояние в Orchestrator) |
| Добавление шагов | Подписка на событие — без изменения кода других сервисов | Нужно менять Orchestrator |
| Циклические зависимости | Возможны (A → B → A) | Контролируются централизованно |
| Единая точка отказа | Нет (каждый сервис независим) | Да (Orchestrator) |
| Отладка | Распределённый trace по логам | Линейный trace в одном месте |
| Сложность компенсаций | Каждый сервис сам обрабатывает ошибки | Централизованная логика в Orchestrator |
Оба подхода имеют право на жизнь. Хореография хороша для простых цепочек (3–4 сервиса) и когда команды хотят оставаться максимально независимыми. Оркестрация — для процессов с ветвлениями, условной логикой и сложными компенсациями.
Когда Saga — не ваш выбор
Saga не серебряная пуля. Вот ситуации, где стоит дважды подумать:
- Нет реальной потребности в микросервисах. Если у вас модульный монолит с одной базой данных, используйте обычные ACID-транзакции. Не усложняйте.
- Бизнес-процесс короткий. Если операция меняет данные в двух сервисах и оба синхронны, Saga с её асинхронной моделью добавит latency без пользы.
- Нет готовности к eventual consistency. После шага 2 заказ может висеть в статусе
PAYMENT_RESERVEDнесколько секунд (и даже минут). Клиент увидит промежуточное состояние. Бизнес должен быть к этому готов.
Заключение
Saga — это не технология, а архитектурное решение. Выбор между хореографией и оркестрацией — это выбор между децентрализованной автономией команд и централизованным контролем бизнес-процесса. Первый вариант хорош, когда сервисов много и они меняются часто. Второй — когда бизнес-логика сложна, а её прозрачность важнее гибкости.
Главное, чему учит Saga: в распределённых системах ошибки — не исключение, а часть нормального режима работы. Проектируя компенсации на этапе архитектуры, вы перестаёте бояться словосочетания «отказ на третьем шаге». Вы к нему готовы.
Никакой магии. Просто дисциплина.