В одной базе всё или ничего обеспечивает ACID-транзакция: либо все шаги применились, либо ни один. Между несколькими сервисами, у каждого своя база, такой транзакции нет — нельзя одной командой откатить чужую базу. Классическое решение «распределённой транзакции», 2PC (two-phase commit), на практике избегают: оно блокирует ресурсы и хрупко падает. Вместо него используют Saga — цепочку локальных транзакций, где у каждого шага есть компенсирующая операция. Если шаг посреди цепочки провалился, систему возвращают не «откатом» (rollback), а компенсациями — отдельными действиями «отмени бронь», «верни деньги». Платят за это eventual consistency: согласованность наступает не мгновенно. Роль аналитика — продумать компенсацию для каждого шага.
Однажды я разбирал инцидент: у клиента списались деньги, а заказ не создался. В монолите такого не бывает — там «списать деньги» и «создать заказ» сидят в одной транзакции, и если второе упало, первое откатывается само. Но систему к тому моменту распилили на сервисы: оплата — отдельный сервис со своей базой, заказы — отдельный со своей. Платёж прошёл, а сервис заказов в этот момент лежал. Деньги ушли, заказа нет, откатывать нечем — у сервиса оплаты нет власти над базой заказов, и наоборот. Тогда до меня дошло: «всё или ничего» между сервисами не дают бесплатно, его надо проектировать руками. Этот механизм называется Saga, и придумывать компенсации в нём — работа аналитика.
Почему ACID-транзакция не работает между сервисами
В одной базе данных есть транзакция со свойствами ACID — про них я подробно писал в записи про базы данных и SQL. Главное здесь — атомарность: группа изменений применяется как одно целое, всё или ничего. Списали деньги и создали заказ внутри одной транзакции — если второе не удалось, СУБД откатывает и первое. Вы получаете «всё или ничего» даром, СУБД гарантирует.
Но это работает, только пока всё в одной базе. Как только вы распилили систему на микросервисы, у каждого появляется своя база. Сервис оплаты не может включить изменение в базе заказов в свою транзакцию — у него нет к ней доступа, это чужая территория. Атомарности через границу сервиса не существует: каждая база коммитит своё отдельно, и нет дирижёра, который откатил бы все базы разом, если на полпути что-то сломалось.
Корень проблемы — одна на всех
«Заказ создан, оплата не прошла» (или наоборот) — это не редкий баг, а прямое следствие того, что у сервисов разные базы. Любой бизнес-процесс, который трогает несколько сервисов — оформление заказа, перевод денег, бронирование, — наступает на этот корень. Поэтому распределённые транзакции это не экзотика для архитекторов, а будничный вопрос, который встаёт сразу после слова «микросервисы».
Почему 2PC избегают
«Классическое» решение существует — two-phase commit (2PC), двухфазная фиксация. Идея: специальный координатор сначала спрашивает все сервисы «готовы зафиксировать?» (фаза подготовки), и только если все ответили «да», командует «фиксируем» (фаза коммита). На бумаге это даёт ту самую атомарность через границы.
На практике 2PC в современных системах почти не используют, и причины две. Первая — блокировки: между фазой «готов» и фазой «коммит» каждый сервис держит свои ресурсы заблокированными, ожидая команду координатора. Пока координатор думает, никто не может тронуть эти данные. Под нагрузкой это убивает пропускную способность. Вторая — хрупкость: если координатор упал между фазами, сервисы зависают в подвешенном состоянии — они уже сказали «готов», но не знают, коммитить или откатывать. Система становится заложником одной точки отказа. Поэтому от 2PC ушли в сторону Saga.
Saga: цепочка локальных транзакций плюс компенсации
Saga переворачивает подход. Вместо «одна большая транзакция на всех» — цепочка маленьких локальных транзакций, по одной в каждом сервисе, каждая фиксируется сразу и независимо. Заказ создаём — коммит в базе заказов. Резервируем товар — коммит в базе склада. Списываем деньги — коммит в базе оплаты. Каждый шаг завершён и зафиксирован прежде, чем начнётся следующий.
Вопрос — что делать, если шаг в середине цепочки провалился, а предыдущие уже зафиксированы. Откатить их (rollback) нельзя: транзакции давно закоммичены, СУБД о них забыла. Ответ Saga: у каждого шага есть компенсирующая операция — отдельное действие, которое семантически отменяет уже сделанное. Не «откати запись о брони», а «создай отмену брони». Не «верни строку в исходное состояние», а «сделай возврат денег». Компенсация — это не rollback, это новое действие, которое приводит мир в порядок.
Компенсация — это «отмени», а не «верни как было»
Ключевое различие, которое путают новички. Rollback в базе делает вид, что транзакции не было вовсе. Компенсация так не умеет — деньги уже ушли, письмо уже отправлено. Компенсация — это бизнес-действие «возврат»: история останется (был платёж, был возврат), но эффект сведётся к нулю. Поэтому компенсацию нельзя «вывести автоматически» — её придумывает тот, кто знает бизнес-смысл шага. Это и есть точка входа аналитика.
Пример: Saga оформления заказа
Возьмём оформление заказа на четыре шага. Для каждого прямого шага продумываем компенсацию — что сделать, если дальше по цепочке что-то сломается.
| Шаг (прямое действие) | Сервис | Компенсация (если упадём дальше) |
|---|---|---|
| Создать заказ (статус «черновик») | Заказы | Отменить заказ (статус «отменён») |
| Зарезервировать товар на складе | Склад | Снять резерв, вернуть остаток |
| Списать оплату | Оплата | Сделать возврат (refund) |
| Подтвердить заказ | Заказы | — |
Сценарий счастливого пути: все четыре шага прошли, заказ подтверждён. Сценарий сбоя: заказ создан, товар зарезервирован, а оплата не прошла (на карте нет денег). Saga запускает компенсации в обратном порядке: снять резерв со склада → отменить заказ. Деньги списать не успели — возвращать нечего. Система пришла в согласованное состояние: заказа нет, товар снова доступен, денег не тронули. Никакой «зависшей» оплаты без заказа.
sequenceDiagram participant O as Заказы participant S as Склад participant P as Оплата O->>O: создать заказ (черновик) O->>S: зарезервировать товар S-->>O: ок, зарезервировано O->>P: списать оплату P-->>O: отказ (нет средств) Note over O,P: запускаем компенсации в обратном порядке O->>S: компенсация: снять резерв S-->>O: резерв снят O->>O: компенсация: отменить заказ
На схеме видно обе половины Saga. Сначала идут прямые шаги: заказы создают черновик и просят склад зарезервировать товар — тот подтверждает. Затем заказы просят оплату списать деньги, и оплата отказывает (нет средств) — это и есть точка сбоя. Дальше Saga разворачивается: вместо того чтобы пытаться откатить уже зафиксированные шаги, она вызывает их компенсации в обратном порядке — сначала просит склад снять резерв (компенсация резервирования), затем сама отменяет заказ (компенсация создания). Каждая компенсация — это отдельное действие, а не магический rollback. В итоге след в истории остаётся, но эффект сведён к нулю.
Хореография против оркестрации
Saga можно собрать двумя способами — это про то, кто дирижирует цепочкой.
Хореография (choreography) — дирижёра нет, сервисы реагируют на события друг друга. Заказы публикуют «заказ создан» → склад это слышит, резервирует, публикует «товар зарезервирован» → оплата слышит, списывает. Это прямое продолжение событийной архитектуры: каждый сам знает, на что реагировать. Плюс — нет единой точки, сервисы развязаны. Минус — общую логику процесса целиком никто не видит, она «размазана» по сервисам, и отлаживать «а где застряло» тяжело.
Оркестрация (orchestration) — есть дирижёр (оркестратор), отдельный компонент, который явно командует: «заказы, создай» → «склад, зарезервируй» → «оплата, спиши», а при сбое сам запускает компенсации. Плюс — вся логика процесса в одном месте, видно целиком, легче менять и отлаживать. Минус — оркестратор это лишний компонент и потенциально узкое место.
Грубое правило: простая цепочка из двух-трёх шагов — хореография; сложный процесс с ветвлениями и много компенсаций — оркестрация, чтобы не потерять нить.
Что меняется для аналитика
Ваша главная работа в Saga — продумать компенсацию для каждого шага, и это не всегда тривиально. «Отменить заказ» — легко. «Вернуть деньги» — а с какой комиссией, сразу или после проверки? «Отозвать уже отправленное письмо клиенту» — никак, письмо ушло; значит, компенсация — прислать второе письмо «извините, заказ отменён». Бывают шаги, которые в принципе не компенсируются (товар уже физически отгружен) — такие ставят в конец цепочки, после точки невозврата. Всё это — решения бизнес-уровня, и принимать их за столом архитектора некому, кроме вас.
Eventual consistency живёт в требованиях
Saga не даёт мгновенного «всё или ничего» — между шагами есть промежутки, когда заказ уже есть, а оплата ещё в процессе, или когда сбой случился, а компенсации ещё крутятся. Это eventual consistency, та же, что в событийной архитектуре. В требованиях это надо проговорить явно: что показывать пользователю, пока Saga в полёте («заказ обрабатывается»), и что показать, если она откатилась («не удалось оформить, деньги вернём в течение N дней»). Если в спеке написано «заказ создаётся мгновенно и атомарно» — а система на Saga, — спека врёт о реальности.
Откуда это взялось
Термин Saga ввели Гектор Гарсиа-Молина и Кеннет Салем в статье 1987 года — задолго до микросервисов. Тогда проблема была другая: «длинные» транзакции в базах данных (например, на часы), которые нельзя держать заблокированными всё это время. Авторы предложили разбить длинную транзакцию на цепочку коротких, каждая со своей компенсацией. Идея пролежала почти без дела четверть века и пережила ренессанс около 2015-х вместе с бумом микросервисов: оказалось, что «много локальных транзакций плюс компенсации» — ровно то, что нужно, когда у каждого сервиса своя база и общей транзакции нет. Старая идея идеально легла на новую боль.
Частые вопросы
Почему нельзя просто сделать одну транзакцию на все сервисы?
Потому что ACID-транзакция работает в пределах одной базы данных, а у каждого микросервиса своя база. Сервис оплаты не имеет доступа к базе заказов и не может включить изменения в ней в свою транзакцию — нет общего «всё или ничего». Распределённый аналог (2PC) существует, но его избегают из-за блокировок и хрупкости. Поэтому «всё или ничего» между сервисами не даётся даром — его проектируют через Saga: цепочку локальных транзакций с компенсациями.
Чем компенсация отличается от отката (rollback)?
Rollback — это механизм базы данных: он делает вид, что транзакции вообще не было, и работает только внутри одной незакоммиченной транзакции. Компенсация — это отдельное бизнес-действие, которое отменяет уже зафиксированный и совершённый эффект: не «верни строку как было», а «сделай возврат денег», «сними бронь», «пришли письмо об отмене». След в истории остаётся (был платёж и был возврат), но итоговый эффект сводится к нулю. Компенсацию нельзя вывести автоматически — её придумывает тот, кто знает бизнес-смысл шага.
Хореография или оркестрация — что выбрать?
Хореография — сервисы реагируют на события друг друга, дирижёра нет; хороша для коротких простых цепочек, но логика процесса размазана и тяжело отлаживается. Оркестрация — отдельный оркестратор явно командует шагами и компенсациями; логика собрана в одном месте, легче менять и отлаживать, но это лишний компонент и потенциальное узкое место. Правило: простая цепочка из 2–3 шагов — хореография; сложный процесс с ветвлениями и множеством компенсаций — оркестрация.