Оформление заказа требует зарезервировать товар, затем подтвердить оплату в разных сервисах. Как провести такую операцию без общей транзакции?
Используйте паттерн Saga: разбейте операцию на локальные транзакции сервисов, связанные событиями или командами, и определите компенсирующие действия для уже выполненных шагов. Например, при отказе оплаты нужно отменить резерв товара.
Saga не создаёт мгновенную атомарность. Она обеспечивает согласованность через последовательность состояний, повторную обработку сообщений и компенсации, поэтому бизнес-операция должна уметь некоторое время находиться в промежуточном состоянии.
В монолитной системе несколько изменений часто выполняются внутри одной транзакции базы данных. При разделении системы на микросервисы данные оказываются в разных хранилищах, а единая транзакция начинает связывать сервисы общей инфраструктурой и снижать их независимость.
Распределённые протоколы вроде двухфазной фиксации позволяют координировать участников, но требуют длительного согласования, усложняют обработку отказов и могут ухудшать доступность. Saga появилась как прикладной способ управлять длительными бизнес-операциями без общей транзакции.
Резерв товара может успешно завершиться, а платёж — отклониться, получить тайм-аут или остаться неизвестным из-за сбоя связи. Если просто повторить всю операцию, можно создать дублирующий платёж или повторно зарезервировать товар.
Если не предусмотреть компенсацию, система оставит некорректное состояние: заказ будет выглядеть оформленным без оплаты либо товар останется недоступным после неуспешного заказа. Поэтому нужно явно описать состояния операции, допустимые переходы, правила повторов и действия при необратимых или неоднозначных сбоях.
Сначала определите владельца процесса — например, сервис заказов. Он создаёт заказ в состоянии ожидает оплаты и инициирует резервирование товара. Сервис склада выполняет локальную транзакцию, сохраняет резерв и сообщает результат.
После успешного резерва сервис заказов инициирует оплату. Платёжный сервис независимо фиксирует результат у себя и сообщает его событием или ответом. При успехе заказ переходит в состояние оплачен; при отказе запускается компенсация — отмена резерва — и заказ переходит в состояние отменён.
Есть два основных варианта координации:
Каждый шаг должен быть идемпотентным: повтор той же команды не должен повторно списывать деньги или создавать новый резерв. Для этого используют устойчивый идентификатор операции, хранят результат обработки и различают повтор запроса, новый запрос и неизвестный результат предыдущей попытки.
Тайм-аут не означает, что операция не выполнилась. Если платёжный сервис не ответил, нужно получить его окончательный статус через безопасную проверку или обработать подтверждение позже, а не немедленно считать платёж неуспешным и запускать компенсацию.
Компенсация не равна откату базы данных. Она является новой бизнес-операцией и тоже может завершиться ошибкой, поэтому ей нужны повторы, контроль состояния, ручное вмешательство для исключительных случаев и наблюдаемость. Кроме того, компенсация может быть частичной: уже отгруженный товар нельзя всегда автоматически вернуть тем же способом.
Интернет-магазин резервировал товар, затем обращался к платёжному сервису. При временном сбое платёжного сервиса команда рассматривала два варианта: синхронно держать запрос открытым до восстановления связи или сразу отменять резерв.
Ожидание давало простой пользовательский сценарий, но увеличивало время блокировки товара и не решало проблему неизвестного результата платежа. Немедленная отмена освобождала товар, однако могла конфликтовать с уже принятым банком платежом.
Был выбран процесс с оркестрацией: заказ получал состояние платёж проверяется, платёжная операция использовала постоянный идентификатор, а окончательный статус запрашивался повторно. Резерв отменялся только после подтверждённого отказа или истечения бизнес-тайм-аута с учётом политики риска.
Такой подход не сделал процесс атомарным, зато исключил автоматическое двойное списание, позволил корректно обрабатывать неопределённые результаты и сделал зависшие операции видимыми для поддержки.
Компенсация должна быть отдельным надёжным процессом, а не одноразовым вызовом в обработчике ошибки. Её состояние нужно сохранять, повторять с ограничением частоты, отображать в мониторинге и передавать на ручное разрешение после исчерпания автоматических попыток. Важно также иметь операцию безопасного повторного запуска: она должна продолжать с текущего состояния, а не выполнять всю Saga заново.
Отказ подтверждён, когда участвующий сервис надёжно сообщил, что локальная операция не выполнена. Тайм-аут, потеря ответа или сбой сети означают только отсутствие знания о результате. В такой ситуации нужно проверить статус по идентификатору операции, дождаться события результата либо перевести процесс в промежуточное состояние; автоматическая компенсация допустима только если бизнес-правила допускают её при неопределённости.
Saga плохо подходит, если промежуточное состояние недопустимо для бизнеса, компенсация невозможна, а окончательное решение требуется атомарно до ответа пользователю. В таком случае следует пересмотреть границы операции, перенести связанные данные под одного владельца или использовать иной механизм координации, включая распределённую транзакцию там, где её цена оправдана. Выбор Saga нельзя обосновывать только тем, что система состоит из микросервисов: решающим является характер бизнес-инварианта и возможность безопасно компенсировать каждый шаг.