Двухфазный коммит блокирует ресурсы и плохо переживает падение координатора. Сага — цепочка локальных шагов плюс явная отмена: вернуть деньги, снять бронь. Это не магический rollback на две базы.
Распределённые системы architecture собеседование
Транзакция на одном Postgres — ACID. Есть общий lock manager и WAL. На двух сервисах с двумя базами «как одна tx» так не работает. Нет общего xmin. Нет атомарного commit на оба диска.
Отсюда два семейства решений. 2PC обещает атомарность ценой блокировок. Сага обещает осмысленный исход ценой промежуточных состояний и ручных компенсаций.
Это не «сага лучше 2PC». Это разные инварианты. Микросервисы почти всегда живут в мире саг, даже если слово не произносят.
Пример. Заказ, оплата, склад — три локальных commit. Если склад не зарезервировал товар, вы не «откатываете транзакцию вселенной». Вы возвращаете деньги и помечаете заказ как неуспешный.
Двухфазный коммит
Фаза prepare: все участники пишут достаточно, чтобы потом закоммитить или откатиться, и голосуют yes. Фаза commit: координатор рассылает решение.
Пока участник in-doubt, он держит блокировки. Строки не отдать другим транзакциям. Если координатор умер после prepare, участники ждут. Иногда до ручного вмешательства.
XA в жизни редка и хрупка: таймауты, эвристический commit, разные драйверы. Для микросервисов за HTTP это почти всегда нет. Вы не удержите lock в БД, пока ждёте соседний REST.
Сага: шаги и компенсации
Каждый шаг — своя локальная транзакция. Откат — не математический undo. Это обратное бизнес-действие: вернуть деньги, снять бронь, отправить письмо.
Некоторые шаги необратимы. SMS уже ушло. Компенсация тогда «извините», а не DELETE.
- Оркестрация. Один процесс знает шаги и статусы. Проще отладка и ретраи. Риск «бога-оркестратора», который знает весь домен.
- Хореография. Событие OrderPlaced сами слушают склад и оплата. Меньше центра. Сложнее цикл, идемпотентность и «кто владеет статусом заказа».
Пример. Бронь отеля прошла, оплата карты упала. Компенсация — отменить бронь, а не надеяться, что две базы сами открутятся назад. Refund тоже может упасть: его ретраят отдельно.
package saga
func (o *OrderSaga) Place(ctx context.Context, cmd PlaceOrder) error {
if err := o.orders.Create(ctx, cmd); err != nil {
return err
}
if err := o.billing.Charge(ctx, cmd.IdempotencyKey, cmd.Amount); err != nil {
return o.orders.MarkFailed(ctx, cmd.ID, err)
}
if err := o.stock.Reserve(ctx, cmd.ID, cmd.Items); err != nil {
_ = o.billing.Refund(ctx, cmd.IdempotencyKey)
return o.orders.MarkFailed(ctx, cmd.ID, err)
}
return o.orders.MarkConfirmed(ctx, cmd.ID)
}
Оркестратор вызывает шаги. Сбой склада — возврат денег, не XA rollback.
Идемпотентность и видимость
Повтор события или ретрай HTTP не должен снять деньги дважды. Нужен ключ идемпотентности и уникальный индекс на payment_id.
Между сервисами нет изоляции. Пользователь увидит «оплачено, но ещё не собрано». Это надо проектировать в API и в UI. Не прятать за словом «транзакция».
Типичная ошибка
Назвать сагу «distributed rollback». Компенсация — новый бизнес-шаг. Refund может упасть. Тогда нужен ретрай компенсации, а не надежда, что 2PC сам раскрутит XA. Второй провал — забыть идемпотентность Charge при at-least-once доставке.
На собеседовании
2PC — блокировка in-doubt, пока координатор молчит. Сага — цепочка локальных commit плюс компенсирующий шаг: вернуть деньги, снять бронь. Скажите, оркестрация это или хореография, и где ключ идемпотентности.