Глава про Saga важна тем, что показывает третий путь согласованности между сервисами: не атомарная распределённая транзакция через блокирующий координатор, а последовательность локальных транзакций T1..Tn, каждая с семантической компенсацией C1..Cn.
На практике это инженерная основа длинных бизнес-процессов поверх архитектуры «база на сервис»: оформление заказа, бронирование, платежи — где 2PC недопустим из-за блокировок, а согласованность переезжает в прикладной слой через хореографию событий или оркестратор-стейтмашину (Temporal, Camunda, Step Functions).
В интервью и архитектурных обсуждениях материал даёт язык, чтобы честно назвать цену: Saga отдаёт ACID-атомарность за отложенную согласованность и теряет изоляцию — отсюда грязные чтения и потерянные обновления, которые приходится лечить semantic lock, идемпотентностью и transactional outbox.
Практическая польза главы
Практика проектирования
Учит разбивать длинную межсервисную транзакцию на локальные шаги с компенсациями и ставить pivot как точку невозврата.
Качество решений
Помогает выбрать между хореографией и оркестрацией по сложности потока и сравнить Saga с 2PC по блокировкам, изоляции и наблюдаемости.
Аргументация на интервью
Даёт чёткую схему: T1..Tn, обратный порядок компенсаций C1..Cn, semantic lock и transactional outbox против dual write.
Риски и компромиссы
Делает явными отсутствие изоляции, уже-видимые эффекты, требование идемпотентности и границу, где лучше согласованная БД.
Контрастная соседняя глава
Распределённые транзакции: 2PC и 3PC
Соседняя глава добивается атомарности нескольких сервисов через блокирующий координатор и фиксацию в две фазы. Эта глава отказывается и от атомарности, и от распределённых блокировок.
Соседняя глава про решает задачу «всё или ничего» поверх нескольких хранилищ через (2PC): координатор спрашивает всех участников «готовы зафиксировать?», и только при единогласном «да» рассылает commit. Результат — настоящая с гарантиями . Цена — каждый участник держит блокировки от фазы prepare до commit, а зависший координатор оставляет всех в неопределённости.
Эта глава про другой полюс. Saga отказывается от единой атомарной транзакции вовсе. Длинная бизнес-операция разбивается на последовательность локальных транзакций T1…Tn — каждая фиксируется в своём сервисе сразу и независимо. Если шаг по дороге падает, Saga не откатывает базы (фиксации уже видны), а запускает компенсирующие транзакции C1…Cn, которыесемантически отменяют сделанное ранее. Никаких распределённых блокировок и единого координатора с правом останавливать всех.
Размен честный: вместо мгновенной атомарности Saga даёт — система проходит через промежуточные состояния, где часть шагов уже зафиксирована, а компенсации ещё не отработали. Зато она масштабируется без блокировок, переживает долгие (часы и дни) бизнес-процессы и не падает целиком из-за одного зависшего узла. Понятие саги ввели Hector Garcia-Molina и Kenneth Salem ещё в 1987 году — задолго до микросервисов.
Saga нужна там, где бизнес-транзакция пересекает границы нескольких сервисов с собственными базами, а держать распределённые блокировки двухфазной фиксации (2PC) недопустимо — из-за масштаба, географии или длительности процесса. Вы меняете атомарность на последовательность локальных коммитов с , принимая отсутствие изоляции как осознанную плату.
Почему не 2PC
Цена атомарности
Двухфазная фиксация (2PC) даёт атомарность, но держит блокировки на всех участниках всё время раунда и зависает при отказе координатора — это плохо масштабируется на длинные бизнес-процессы.
Проблема: согласованность без распределённой транзакции
В архитектуре «база на сервис» у заказа, склада и платежей — свои хранилища. Бизнес хочет, чтобы «оформить заказ» был атомарным: либо все три шага прошли, либо ни один. Очевидное решение — обернуть всё в одну распределённую транзакцию через двухфазную фиксацию (2PC). Но на масштабе и на длинных процессах это решение ломается по трём направлениям.
Блокировки на всё время раунда
Между prepare и commit каждый участник держит ресурсы заблокированными. Чем больше сервисов и чем дольше раунд, тем выше конкуренция за ресурсы (contention): конкурентные транзакции встают в очередь, пропускная способность (throughput) падает.
Блокирующий координатор
Если координатор упал после prepare, но до commit, участники остаются в неопределённости (in-doubt): ресурсы заблокированы, а решение неизвестно. Трёхфазная фиксация (3PC) смягчает это, но не убирает блокировки полностью.
Длинные бизнес-процессы
Бронь, проверка кредита, доставка могут занимать минуты, часы, дни. Держать транзакционные блокировки всё это время невозможно — они задушат базы задолго до завершения процесса.
Ключевой сдвиг мышления: Saga не пытается сохранить иллюзию одной атомарной транзакции. Она принимает, что промежуточные состояния видны другим, и переносит ответственность за «откат» из движка БД в прикладную логику. То, что двухфазная фиксация (2PC) прячет за блокировками, Saga выносит наружу как явные компенсации — и платит за это потерей изоляции.
Первоисточник
Sagas (1987)
Garcia-Molina и Salem ввели понятие саги для long-lived transactions: разбить долгую транзакцию на цепочку коротких, чтобы не держать блокировки часами.
Что такое Saga: T1…Tn и компенсации C1…Cn
В статье «Sagas» (Garcia-Molina, Salem, ACM SIGMOD, 1987) сага определена как способ разбить long-lived transaction (долгоживущую транзакцию, LLT) на последовательность коротких локальных транзакций T1, T2, …, Tn, каждая из которых фиксируется немедленно. Для каждой Ti определена Ci, семантически отменяющая её эффект. Сага гарантирует один из двух исходов:
Полный успех
Выполнилась вся последовательность T1 … Tn. Бизнес-операция завершена, каждый сервис зафиксировал свою часть локально. Это «прямой» путь саги.
Компенсированный откат
Выполнились T1 … Tj, шаг Tj+1 провалился. Тогда отрабатывают компенсации Cj … C1 в обратном порядке. Итог семантически эквивалентен «как будто ничего не было».
Прямой путь и компенсированный откат (провал на T3)
Сверху — успешная сага T1→T2→T3. Снизу — провал T3: запускаются компенсации C2 и C1 в обратном порядке, возвращая систему в бизнес-эквивалент исходного состояния.
Это не в смысле , а атомарность саги: гарантия, что система не зависнет «на полпути» навсегда — она либо дойдёт до конца, либо корректно компенсируется назад. Важная классификация (её закрепили Microsoft и Chris Richardson): шаги делятся на компенсируемые (compensable — можно отменить через Ci), pivot (точка невозврата: после неё компенсации уже неуместны) и повторяемые (retryable — идемпотентны, выполняются после pivot до победного).
Соседняя глава
Оркестрация рабочих процессов
Оркестратор Saga при росте сложности превращается в долговечный (durable) движок процессов: стейт-машина, история шагов, таймеры, повторы — ровно то, что дают Temporal и Camunda.
Хореография против оркестрации
Сагой нужно как-то управлять: кто-то должен решать, какой шаг следующий и когда запускать компенсации. Есть два полярных способа координации, и выбор между ними — это размен связности на наблюдаемость.
: реакция на события
Центрального дирижёра нет. Каждый сервис, зафиксировав локальную транзакцию, публикует , а другие подписчики реагируют на него своими шагами. Логика саги «размазана» по участникам.
Плюс: нет единой точки отказа, слабая связность, просто для коротких потоков. Минус: при росте числа шагов трудно понять, кто на что реагирует; риск циклических зависимостей; тяжело тестировать и наблюдать процесс целиком.
: центральный дирижёр
Появляется — стейт-машина, которая хранит состояние саги, рассылает участникам команды («сделай Ti»), ждёт ответов и при провале сама запускает компенсации в нужном порядке.
Плюс: логика процесса в одном месте, отличная наблюдаемость, легко добавлять шаги, нет циклов. Минус: оркестратор — потенциальная точка отказа и центр связности; нужен отдельный сервис/движок.
Практическое правило: хореография хороша для 2–4 шагов с простой логикой, где главное — слабая связность. Как только появляются ветвления, условные компенсации и требование «видеть процесс целиком», выгоднее оркестрация — и обычно её реализуют не вручную, а на долговечном (durable) движке из соседней главы про оркестрацию процессов.
Компенсации: семантический откат, а не rollback БД
Компенсация — это не ROLLBACK базы. Локальная транзакция Ti уже зафиксирована и видна другим; «отменить» её можно только новой транзакцией Ci, которая делает обратное по смыслу. Отсюда несколько жёстких требований к компенсациям.
Семантический, а не точный откат
Ci возвращает систему не в байт-точную копию прошлого, а в бизнес-эквивалентное состояние. Списали деньги — компенсация делает возврат (refund), а не «отматывает» баланс: в истории останутся обе проводки. Зарезервировали товар — компенсация снимает резерв.
Из-за повторов и ненадёжной доставки и Ti, и Ci могут прийти дважды. Повторное применение обязано давать тот же результат, что и однократное, — иначе двойной возврат платежа или двойное списание. Идемпотентность здесь не опция, а условие корректности.
Коммутативность с прямым шагом
Компенсация может прийти раньше, чем подтверждение прямого шага (гонки доставки). Хороший дизайн делает Ci устойчивой к этому: например, «отменить резерв №X» корректно работает, даже если резерв ещё не успел отразиться, — иначе придётся вводить ожидание и буферизацию.
Проблема уже-видимых эффектов
Самое тяжёлое: эффект шага мог уже «утечь» наружу. Письмо клиенту отправлено, товар уехал со склада, деньги ушли контрагенту. Чистой компенсации нет — нужны бизнес-меры: уведомление, возвратная логистика, ручная эскалация. Поэтому необратимые шаги делают pivot и ставят как можно позже.
Отсюда практический порядок шагов: сначала всё компенсируемое (бронь, резерв, проверка), затем единственный pivot (точка невозврата — например, фактическая отгрузка), и после него только повторяемые идемпотентные шаги, которые гарантированно доведутся до конца. Так зона, где может потребоваться откат, остаётся обратимой.
Чего нет
Изоляция (ACID-I)
Двухфазная фиксация (2PC) и блокировки дают изоляцию; Saga её не даёт вовсе. Промежуточные состояния видны конкурентным сагам — отсюда грязные чтения и потерянные обновления.
Аномалии изоляции: чего Saga не даёт
Saga жертвует буквой «I» из : изоляции между сагами нет. Раз каждый шаг фиксируется сразу, его промежуточный результат виден другим транзакциям до того, как сага завершилась или компенсировалась. Это порождает классические аномалии — те же, что в слабых БД.
Грязное чтение (dirty read)
Сага B читает данные, которые сага A уже записала локально, но потом откатит компенсацией. B приняла решение на основе того, чего «не будет».
Потерянное обновление (lost update)
A и B параллельно читают и перезаписывают одну запись, не видя изменений друг друга. Одно из обновлений молча затирается.
Неповторяемое чтение
Разные шаги одной саги читают одну сущность и получают разные значения, потому что между чтениями её изменила другая сага.
Контрмеры (countermeasures по Microsoft / Richardson)
- Semantic lock (семантическая блокировка): шаг ставит флаг «в обработке» (например, статус заказа PENDING), и другие саги знают, что данные ещё не финальны.
- Commutative updates (коммутативные обновления): операции проектируют так, чтобы порядок применения не влиял на итог (инкремент вместо «прочитал-записал»).
- Pessimistic view: переупорядочить шаги так, чтобы обновления попадали в retryable-фазу после pivot — тогда грязных чтений не будет.
- Reread value (перечитывание): перед записью проверить, что данные не изменились с момента чтения; если изменились — прервать и перезапустить шаг.
- By value: выбирать механизм по бизнес-риску запроса — для дешёвых операций сага, для дорогих и критичных всё же согласованная транзакция.
Надёжная доставка: outbox, CDC и идемпотентные ключи
У саги есть коварная щель: шаг должен и зафиксировать данные в своей БД, и послать событие/команду следующему. Сделать это атомарно нельзя — БД и брокер сообщений разные системы (та самая ). Упадём между ними — либо данные есть, а событие потеряно (сага зависла), либо событие ушло, а данных нет. Закрывает щель связка из журнала исходящих (outbox) и захвата изменений данных (CDC).
+ CDC
Событие пишут в ту же локальную транзакцию, что и бизнес-данные, — в специальную таблицу outbox. Раз это один коммит, либо есть и данные, и запись о событии, либо нет ничего. Атомарность двойной записи сведена к обычной локальной транзакции.
Дальше отдельный релей вычитывает outbox и публикует события в брокер — через (чтение лога транзакций, например Debezium) или публикатор на опросе ().
-эффект через идемпотентность
Брокеры дают : событие может прийти повторно. Истинной доставки «ровно один раз» нет, но нужен эффект «ровно один раз». Его добивают .
Каждое событие несёт уникальный ключ (например, sagaId + stepId). Потребитель хранит обработанные ключи и при повторе просто игнорирует дубликат. Так ретраи безопасны: эффект применяется ровно один раз, даже если сообщение доставлено многократно.
Вместе outbox и идемпотентность закрывают надёжность саги: outbox гарантирует, что событие не потеряется (фиксация атомарна с данными), а идемпотентные ключи — что повтор не навредит. Без этой пары любая сеть рано или поздно либо подвесит сагу, либо выполнит шаг дважды.
Транспорт
Межсервисное взаимодействие
Хореографическая сага живёт на событиях в Kafka; оркестрированная — на командах от движка к участникам. И то, и другое — паттерны межсервисной связи из соседней главы.
Инструменты: оркестраторы и событийная модель (event-driven)
Сагу почти никогда не пишут «с нуля»: координацию, долговечное () состояние, таймеры и повторы дают готовые движки. Грубо инструменты делятся на два лагеря — долговечные оркестраторы и событийные шины.
| Инструмент | Стиль | Как реализует сагу |
|---|---|---|
| Temporal | Оркестрация (код) | Долговечное исполнение (durable execution): код рабочего процесса детерминированно воспроизводится из истории событий; компенсации описывают как явные шаги отката, движок гарантирует их запуск при сбое. |
| Camunda / Zeebe | Оркестрация (нотация BPMN) | Сага как процесс в нотации BPMN с compensation boundary events: моделируете прямые шаги и привязанные к ним компенсации визуально, движок исполняет стейт-машину. |
| AWS Step Functions | Оркестрация (конечный автомат) | Стандартный рабочий процесс как saga-оркестратор: шаги вызывают сервисы, при ошибке Catch-переходы запускают компенсирующие задачи (Revert Payment, Revert Inventory). |
| Apache Kafka | Хореография (события) | Транспорт для событийной (event-driven) саги: каждый сервис публикует событие в топик, остальные реагируют; порядок и долговечность (durability) держит сам лог, координатора нет. |
Граница простая: долговечные оркестраторы (Temporal, Camunda/Zeebe, Step Functions) дают наблюдаемость, явные компенсации и историю выполнения — выбор для сложных саг. Событийный (event-driven) подход через Kafka даёт слабую связность и отсутствие единой точки отказа — выбор для простых хореографических потоков. Подробнее про сами движки — в соседней главе про оркестрацию процессов.
Компромиссы: когда Saga, а когда согласованная БД
Saga подходит, когда
- Бизнес-транзакция пересекает несколько сервисов с собственными базами — двухфазная фиксация (2PC) недоступна или дорога.
- Процесс длинный (минуты/часы/дни) и держать блокировки всё это время невозможно.
- Домен допускает и промежуточные видимые состояния.
- Для каждого шага есть осмысленная семантическая компенсация (возврат платежа, отмена резерва, отмена брони).
Согласованная БД / 2PC лучше, когда
- Все данные операции живут в одной базе — берите обычную локальную транзакцию с гарантиями , не усложняйте.
- Нужна и изоляция: нельзя, чтобы кто-то увидел промежуточное состояние.
- Шаги необратимы и компенсации не имеют смысла, а риск ошибки слишком высок (by value).
- Команда не готова сопровождать компенсации, идемпотентность, outbox и оркестратор.
Частые ошибки
- Считать компенсацию БД. Ci — отдельная транзакция, она семантически отменяет, а не «отматывает»; уже-видимые эффекты ею не убрать.
- Забыть про идемпотентность. Без идемпотентных ключей доставка «как минимум один раз» (at-least-once) даёт двойные списания и двойные компенсации.
- Игнорировать аномалии изоляции. Saga не даёт «I» из ; без semantic lock и коммутативных обновлений будут грязные чтения и потерянные обновления.
- Тянуть Saga туда, где хватает одной БД. Если данные в одном хранилище, распределённая сага — это лишняя сложность без выгоды.
- Не доставлять события атомарно. Двойная запись без outbox рано или поздно подвесит сагу на потерянном событии.
Что важно запомнить
- Saga (Garcia-Molina, Salem, 1987) разбивает длинную транзакцию на цепочку локальных T1..Tn, каждая с компенсацией C1..Cn, и даёт отложенную согласованность вместо атомарности ACID.
- В отличие от соседней двухфазной фиксации (2PC), Saga не держит распределённых блокировок и не зависит от блокирующего координатора — поэтому масштабируется на длинные межсервисные процессы.
- Хореография (события, слабая связность) против оркестрации (центральный дирижёр, наблюдаемость) — выбор по сложности потока.
- Компенсация — семантический откат, а не ROLLBACK: должна быть идемпотентной, по возможности коммутативной, и бессильна против уже-видимых эффектов (отсюда pivot).
- Saga жертвует изоляцией: грязные чтения и потерянные обновления лечат semantic lock, коммутативными обновлениями, перечитыванием и выбором by value.
- Надёжность держится на транзакционном журнале исходящих событий (transactional outbox) + CDC (событие не потеряется) и идемпотентных ключах (повтор не навредит); брокер даёт доставку «как минимум один раз» (at-least-once), ключи — эффект «ровно один раз».
Источники и материалы
Карта источников: Garcia-Molina/Salem — первоисточник про T1..Tn и компенсации; microservices.io — modern choreography/orchestration и transactional outbox; Azure и AWS — cloud pattern guidance. Компенсация в Saga — бизнес-эквивалентный обратный шаг, а не автоматический rollback, поэтому exactly-once-эффект требует идемпотентности, outbox/CDC и явной модели повторов.
Связанные главы
- Распределённые транзакции: 2PC и 3PC - Контрастный соседний подход: атомарность через блокирующий координатор и фиксацию в две/три фазы вместо последовательности локальных транзакций с компенсациями.
- Интеграция микросервисов: обзор - Помещает Saga в общую карту интеграции: где база-на-сервис делает распределённую транзакцию невозможной и почему согласованность переезжает в прикладной слой.
- Паттерны межсервисного взаимодействия - Показывает транспорт, на котором держится Saga: события, команды, очереди и асинхронная доставка между шагами длинной транзакции.
- Паттерны оркестрации рабочих процессов - Разворачивает оркестратор Saga в полноценный движок процессов: стейт-машины, надёжное исполнение с гарантией долговечности (durable execution), таймеры и история выполнения в Temporal/Camunda.
