Компенсирующие транзакции: разбор сбоя в заказах
Платёж прошёл, склад зарезервировал товар, а доставка не создала отправление. В такой цепочке нельзя откатить уже выполненные действия одной командой: нужно спроектировать компенсации, идемпотентность и восстановление после частичного сбоя.

Платёжный сервис списал деньги, склад зарезервировал товар, но сервис доставки не создал отправление. Это типичный частичный сбой распределённой операции: общей транзакции, которая разом откатит три независимые системы, нет. Ниже — разбор условного инцидента в цепочке заказа: цифры производительности не приводятся, потому что без конкретной системы и замеров они были бы выдумкой.
Что произошло: заказ завис между оплатой и доставкой
Представим последовательность: магазин создаёт заказ, платёжный сервис подтверждает списание, склад уменьшает доступный остаток и резервирует товар, затем сервис доставки принимает запрос на создание отправления. Последний шаг завершается тайм-аутом.
Тайм-аут не доказывает, что операция не выполнилась. Запрос мог дойти до службы доставки, а ответ потеряться. Если магазин просто повторит запрос, он рискует создать две отправки. Если сразу вернёт деньги, но отправление всё-таки создалось, в системах появится заказ, который уже отправляют, хотя покупателю сообщили об отмене.
Локальный откат базы магазина здесь не поможет: платёж и резервирование уже зафиксированы в других сервисах. Нужен бизнес-процесс, который знает, какие действия успели завершиться и чем компенсировать каждое из них.
Компенсация — не машина времени. Она создаёт новое действие, которое исправляет бизнес-результат предыдущего.
Как устроена компенсация
В саге* длинная операция разбивается на локальные транзакции. После успешного шага система запускает следующий. Если продолжить нельзя, она выполняет подходящие компенсирующие действия в обратном порядке — но только там, где это ещё допустимо.
Для условного заказа сценарий может выглядеть так:
- Создать заказ в состоянии «ожидает оплаты».
- Подтвердить платёж и сохранить идентификатор операции.
- Зарезервировать товар и сохранить номер резерва.
- Запросить отправление у службы доставки.
- Если отправление не создано, выяснить результат запроса, а затем отменить резерв и инициировать возврат платежа, если заказ действительно отменяется.
Порядок компенсаций не всегда буквально обратный. Например, возврат денег может выполняться позже отмены резерва, если политика бизнеса требует сначала подтвердить отмену товара. Это решение нужно зафиксировать в правилах процесса, а не оставлять случайному порядку обработки сообщений.
Сага* — последовательность локальных транзакций в нескольких сервисах, где согласованность достигается продолжением процесса или отдельными компенсирующими операциями, а не общей блокировкой всех баз данных.
Что должно было сработать при тайм-ауте
Сначала выяснить, состоялась ли операция
Повторять создание отправления безопасно только при наличии устойчивого ключа идемпотентности или способа запросить результат по идентификатору операции. Иначе повтор может создать дубль. Если внешний сервис не поддерживает ни идемпотентный запрос, ни проверку состояния, неопределённость придётся обрабатывать отдельной сверкой и ручным разбором.
Не терять факт завершённого шага
Система должна сохранять прогресс саги: заказ, выполненные шаги, идентификаторы внешних операций и состояние компенсации. Если процесс упал после списания, новый воркер должен понимать, что списание уже было, а не отправлять платёжный запрос заново.
Для надёжной передачи событий обычно отдельно решают проблему двойной записи: изменение состояния и публикация сообщения не должны расходиться. Один из распространённых вариантов — записать событие в ту же локальную базу, а затем доставить его брокеру. Выбор механизма зависит от используемого хранилища и брокера; конкретная конфигурация здесь не приводится, потому что тема не привязана к одной версии стека.
Считать компенсацию отдельной операцией
Отмена резерва или возврат платежа тоже могут завершиться тайм-аутом, повториться или временно стать невозможными. Для компенсации нужны собственный идентификатор, повторяемая обработка и состояние вроде «ожидает», «выполняется», «завершена», «требует вмешательства». Ошибка компенсации не должна молча превращать заказ в обычную отмену.
Где подход ломается
Компенсация не всегда полностью возвращает исходное состояние. Уже отправленную посылку нельзя отменить тем же способом, каким снимают складский резерв. Возврат денег может занять время, а внешняя система может отклонить его из-за собственных правил. Поэтому сагу стоит проектировать с учётом бизнес-состояний после каждого шага, а не как набор симметричных вызовов «сделать» и «отменить».
- Повторная доставка сообщения. Обработчик получил событие, выполнил действие, но не подтвердил получение. Сообщение придёт снова. Обработчик должен распознать уже обработанную операцию.
- Запоздалый ответ. После тайм-аута система начала компенсацию, а затем пришло подтверждение исходного действия. Нужны проверки состояния и правила, не позволяющие позднему ответу оживить отменённый заказ.
- Компенсация тоже недоступна. Платёжный сервис может не принимать возвраты или склад — отмену резерва. Процесс должен оставаться видимым для повторной попытки и ручного разбора.
- Два процесса меняют один заказ. Покупатель отменил заказ одновременно с автоматической обработкой доставки. Нужны контроль допустимых переходов и защита от конкурирующих команд.
- Слишком длинная сага. Чем больше шагов и внешних систем, тем больше промежуточных состояний и сценариев отказа. Иногда полезно упростить процесс или вынести необязательное действие за пределы критического пути.
Что измерять вместо одного показателя успешности
Без замеров нельзя честно сказать, насколько компенсации ускорят восстановление или увеличат нагрузку. Для конкретного инцидента сначала соберите временную шкалу и данные по каждому шагу: время запроса, ответ сервиса, повтор, итоговая проверка состояния. Затем сравнивайте периоды при одинаковых объёме заказов, составе сервисов и нагрузке.
| Что смотреть | Что это покажет | На что реагировать |
|---|---|---|
| Возраст незавершённых саг | Как долго заказы остаются в промежуточных состояниях | Растущий хвост старых процессов |
| Доля успешных компенсаций | Как часто система сама доводит отмену до конца | Повторяющиеся отказы конкретного сервиса или шага |
| Повторы и дубли операций | Насколько безопасна повторная доставка | Повторные списания, резервы или отправления |
| Ручные разборы и сверки | Какую часть восстановления система не закрывает автоматически | Рост очереди и повторяющиеся причины эскалаций |
В логах и трассировках связывайте шаги общим идентификатором процесса и отдельно записывайте идентификаторы внешних операций. Не кладите в логи платёжные секреты и персональные данные без необходимости. Важно видеть не только ошибку, но и то, было ли действие подтверждено внешней системой.
Как выбирать между сагой, откатом и ручным разбором
| Подход | Когда подходит | Цена |
|---|---|---|
| Локальная транзакция | Все изменения принадлежат одной базе и одной атомарной операции | Не решает согласование независимых сервисов |
| Сага с компенсациями | Шаги выполняются в разных сервисах, а бизнес допускает исправляющие действия | Нужно сопровождать промежуточные состояния, повторы и отказ компенсации |
| Сверка и ручная очередь | Операция необратима или внешняя система не позволяет надёжно определить результат | Восстановление медленнее, нужны ответственные и процедура разбора |
Для заказа разумно автоматизировать те компенсации, которые можно безопасно повторять и проверять. Необратимые действия оставляйте за явной границей процесса: если груз уже передан перевозчику, вместо «отменить отправление» может понадобиться отдельный сценарий возврата.
Что делать дальше
Возьмите один реальный заказ, который завис или был обработан дважды, и восстановите цепочку событий по идентификаторам. Отметьте, какие действия уже состоялись, какие можно повторить, а какие требуют сверки. Затем добавьте тесты на тайм-аут после успешного внешнего действия, повторное сообщение, поздний ответ и отказ компенсации. После запуска следите за возрастом незавершённых процессов и долей ручных разборов — они покажут, где система всё ещё оставляет работу людям.
Разбор описывает общий подход для распределённых систем и не привязан к конкретной версии библиотеки, фреймворка или брокера сообщений. Инвалидация кэша и гарантии доставки сообщений в конкретном брокере — отдельные темы.
Контент-конвейер: Из одной идеи — текст, изображение, озвучка и ролик. Один заказ вместо четырёх.
Спросить за 60 ₽Источники: Saga pattern — AWS Prescriptive Guidance, Saga distributed transactions pattern — Microsoft Azure Architecture Center
Частые вопросы
Компенсирующая транзакция полностью откатывает исходное действие?
Можно ли просто повторить запрос после тайм-аута?
Что делать, если сама компенсация завершилась ошибкой?
Нужна ли сага, если сервисов всего два?
Какие данные нужны для расследования инцидента?
Как понять, что после изменений стало лучше?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.