Saga: распределённые транзакции без двухфазного коммита

Как паттерн Saga решает проблему распределённых транзакций, чем оркестрация отличается от хореографии и когда saga создаёт больше проблем, чем решает

В монолите транзакция — это BEGIN ... COMMIT. Все операции либо выполнились, либо нет. Когда система распадается на несколько сервисов с разными базами, одна транзакция перестаёт покрывать весь процесс.

Классическое решение — двухфазный коммит (2PC) — работает, но плохо масштабируется: координатор можно сделать отказоустойчивым, но сам протокол остаётся блокирующим и дорогим, держит блокировки на всех участниках до завершения и плохо ложится на автономные сервисы с независимыми хранилищами.

Saga — альтернативный подход: вместо одной большой транзакции выполняется последовательность локальных транзакций. Если одна из них падает, предыдущие откатываются через компенсирующие действия. Нет глобальных блокировок, нет координатора в классическом смысле — но появляются свои компромиссы.

В статье

Что такое saga и зачем она нужна

Saga — это последовательность шагов, где каждый шаг:

  1. Выполняет локальную транзакцию в своём сервисе.
  2. Публикует событие или вызывает следующий шаг.
  3. Имеет компенсирующее действие на случай отката.

Если шаг N упал, система выполняет компенсации для шагов N−1, N−2, … , 1 в обратном порядке.

Важно: saga не даёт ACID-изоляции. Промежуточные состояния видны другим процессам. Это фундаментальное отличие от 2PC, и с ним придётся работать — но не вслепую: видимость можно ограничить контрмерами (см. ниже).

Оркестрация vs хореография

Оркестрация

Центральный оркестратор управляет последовательностью шагов. Он знает весь flow, вызывает участников, обрабатывает ответы и запускает компенсации при ошибке.

Плюсы:

  • логика процесса в одном месте;
  • легко отследить текущее состояние saga;
  • проще добавлять шаги и менять порядок.

Минусы:

  • оркестратор становится критичным компонентом;
  • участники зависят от оркестратора;
  • риск превратиться в «умный координатор, глупые сервисы».

Хореография

Нет центрального управления. Каждый сервис слушает события и решает, что делать дальше. Цепочка действий возникает из взаимодействия, а не из плана.

Плюсы:

  • нет single point of failure;
  • сервисы максимально независимы;
  • легко добавлять подписчиков.

Минусы:

  • сложно понять полный flow — он «растворён» в событиях;
  • трудно отлаживать и мониторить;
  • циклические зависимости могут возникнуть незаметно.

Что выбрать

Оркестрация — когда процесс линейный, шагов больше трёх и нужен чёткий контроль. Хореография — когда участников мало, связи простые и важна независимость сервисов.

Компенсирующие действия: откат без rollback

Компенсация — это не ROLLBACK. Это новое действие, которое отменяет эффект предыдущего шага.

Примеры:

Шаг Компенсация
Списать средства со счёта Вернуть средства на счёт
Зарезервировать товар на складе Снять резерв
Отправить уведомление Отправить отмену (не всегда возможно)

Важные свойства:

  • компенсация должна быть идемпотентной — повторный вызов не должен менять результат;
  • не все действия можно компенсировать (отправленное письмо не отозвать);
  • порядок компенсаций — обратный порядку шагов.

Лунная база: saga при распределении аварийных ресурсов

Аварийный сценарий на лунной базе: разгерметизация модуля, нужно перераспределить ресурсы.

Шаги saga:

  1. Логистика: зарезервировать аварийный запас кислорода.
  2. Энергетика: переключить энергоснабжение на аварийный режим.
  3. Робототехника: отправить ремонтного робота к модулю.
  4. Жизнеобеспечение: активировать альтернативный контур подачи воздуха.

Если шаг 3 падает (робот недоступен):

  • Компенсация шага 2: вернуть энергоснабжение в штатный режим.
  • Компенсация шага 1: снять аварийный резерв.
  • Эскалация: уведомить оператора для ручного решения.

Обратите внимание: шаг 4 не начнётся, пока не завершён шаг 3. А компенсация для «отправить робота» может быть «отозвать задание» — если робот ещё не выехал.

Идемпотентность: почему без неё saga не работает

В распределённой системе сообщения могут доставляться повторно. Если шаг saga не идемпотентен, повторная доставка сообщения выполнит его дважды.

Идемпотентность достигается через:

  • уникальный идентификатор операции — каждый шаг проверяет, не выполнялся ли он уже с этим ID;
  • idempotency key в запросе;
  • условные обновленияUPDATE ... WHERE status = 'pending'.

Без идемпотентности saga превращается в генератор дублирующих операций.

Семантика «at least once» и дубликаты

Семантика доставки зависит от конкретного брокера и его конфигурации (at-most-once, at-least-once, реже — effectively-once). Для saga мы сознательно выбираем at-least-once как гарантию транспорта: сообщение может прийти повторно, но не потеряется. Именно этот выбор и делает идемпотентность обязательной — за неё, а не за «магию брокера», отвечает наш код.

Для saga это означает:

  • каждый обработчик должен быть готов к повторному вызову;
  • компенсации тоже должны быть идемпотентными;
  • outbox pattern помогает гарантировать, что событие опубликовано ровно тогда, когда транзакция закоммичена.

Контрмеры: как жить с видимостью промежуточных состояний

Отсутствие изоляции — не приговор. Есть набор приёмов (countermeasures), которые ограничивают вред от видимости незавершённой саги:

  • Semantic lock — шаг помечает запись флагом «в процессе» (status = pending), и другие процессы знают, что значение неокончательное. Самый частый и практичный приём;
  • Commutative updates — операции проектируются так, что порядок применения не важен (например, прибавить/убавить, а не «установить значение»), тогда промежуточная видимость не ломает результат;
  • Pessimistic view — порядок шагов выстраивается так, чтобы «опасные» с точки зрения видимости операции выполнялись последними;
  • Re-read value — перед изменением шаг перечитывает запись и проверяет, что она не менялась с момента чтения (защита от перезаписи чужой саги).

Минимум, который стоит закладывать почти всегда, — semantic lock: явный признак незавершённости избавляет от целого класса гонок «прочитали половину саги как финальное состояние».

Где saga не подходит

  • Нужна строгая изоляция — saga не даёт ACID isolation. Промежуточные состояния видны. Если это недопустимо, нужен 2PC или пересмотр границ сервисов.
  • Много участников с длинными компенсациями — чем длиннее цепочка, тем сложнее и ненадёжнее откат.
  • Действия необратимы — если компенсация невозможна (физическое действие, отправка средств на внешний счёт), saga не решает проблему полностью.
  • Простой домен — если транзакция помещается в одну базу, saga — это переусложнение.

Checklist: готовы ли вы к saga

  1. Действительно ли процесс пересекает границы нескольких сервисов с разными хранилищами?
  2. Определены ли компенсирующие действия для каждого шага?
  3. Идемпотентны ли все шаги и компенсации?
  4. Выбран подход: оркестрация или хореография?
  5. Есть ли мониторинг состояния саг (какие висят, какие застряли)?
  6. Продумана ли эскалация для случаев, когда компенсация невозможна?
  7. Допустима ли видимость промежуточных состояний — и если нет, заложены ли контрмеры (semantic lock и др.)?

Если на пункт 1 ответ «нет» — вам не нужна saga. Если на пункты 2–3 ответ «не знаем» — вы не готовы к saga.

Документация и первоисточники

Обсуждение в Telegram

Присоединиться →

Комментарии