> Как гарантировать, что заказ не будет выполнен без исполнителя после резерва товара (Go)
Уровень: senior · Роль: backend · Категория: Технические вопросы
Компании: sferaplatform.ru
Стек: Go
> Пример ответа
Короткий ответ
Гарантия "заказ не выполнен без исполнителя" - это задача атомарности и консистентности между резервом товара и назначением исполнителя. Решение - использовать транзакцию с блокировками (SELECT ... FOR UPDATE) или распределённую транзакцию (Saga, Outbox) в зависимости от архитектуры. Ключевое: резерв товара и фиксация исполнителя должны быть в одной логической единице, либо резерв должен быть "мягким" с таймаутом и компенсацией, если исполнитель не найден. В Go - это паттерн с использованием database/sql и context.
Подробное объяснение
Проблема возникает, когда резерв товара и назначение исполнителя выполняются в разных сервисах или в разных транзакциях. Если резерв прошёл, а исполнитель не назначен (например, из-за сбоя), заказ "зависает": товар заблокирован, но работа не выполняется.
Основные подходы:
-
Локальная транзакция - если оба действия в одной БД: BEGIN; SELECT ... FOR UPDATE по товару; INSERT заказа с исполнителем; UPDATE резерва; COMMIT. Блокировка гарантирует, что другой процесс не изменит состояние до завершения.
-
Распределённая транзакция (Saga) - если сервисы разные. Шаги: резерв товара → назначение исполнителя. При сбое на втором шаге - компенсация (отмена резерва). Требует идемпотентности и журналирования.
-
Outbox + retry - событие "заказ создан" пишется в ту же БД, что и резерв. Отдельный воркер читает outbox, назначает исполнителя, при ошибке повторяет. Резерв не снимается, пока не будет подтверждения.
-
Мягкий резерв с TTL - резерв действует N минут. Если исполнитель не назначен за это время, резерв автоматически снимается, заказ переходит в статус "требует подтверждения".
Выбор зависит от требований к консистентности: строгая (транзакция) или eventual (Saga/Outbox). Для high-load систем предпочтительнее Saga или Outbox, так как длительные блокировки строк убивают производительность.
На практике
В Go с PostgreSQL типичный сценарий - использование pgx или database/sql с уровнем изоляции ReadCommitted и FOR UPDATE. Важно: блокировка должна быть короткой, все внешние вызовы (HTTP к сервису исполнителей) - вне транзакции. Иначе - deadlock и падение производительности.
Паттерн:
- Открыть транзакцию.
SELECT ... FOR UPDATEпо товару (или по заказу).- Проверить, что товар доступен и резерв не просрочен.
- Вставить запись о заказе с исполнителем (или обновить статус).
- Commit.
Если назначение исполнителя - внешний вызов, то:
- сначала резерв с TTL;
- затем вызов сервиса исполнителей;
- при успехе - подтверждение и снятие TTL;
- при неудаче - компенсация (отмена резерва) или ожидание retry.
Для распределённой системы - использовать Saga-координатор (например, Temporal, Cadence) или самописный outbox с таблицей outbox_events.
Пример кода
GO// Резерв товара + назначение исполнителя в одной транзакции (PostgreSQL)func ReserveAndAssign(ctx context.Context, db *sql.DB, orderID, itemID, executorID string) error {tx, err := db.BeginTx(ctx, nil)if err != nil {return err}defer tx.Rollback()// Блокируем строку товараvar available boolerr = tx.QueryRowContext(ctx,`SELECT available FROM items WHERE id = $1 FOR UPDATE`, itemID,).Scan(&available)if err != nil {return err}if !available {return errors.New("item not available")}// Резервируем товар_, err = tx.ExecContext(ctx,`UPDATE items SET available = false, reserved_until = now() + interval '10 minutes' WHERE id = $1`, itemID)if err != nil {return err}// Создаём заказ с исполнителем_, err = tx.ExecContext(ctx,`INSERT INTO orders (id, item_id, executor_id, status) VALUES ($1, $2, $3, 'assigned')`,orderID, itemID, executorID)if err != nil {return err}return tx.Commit()}
Для Saga - пример с компенсацией:
GOfunc ReserveAndAssignSaga(ctx context.Context, orderID, itemID, executorID string) error {// Шаг 1: резервif err := reserveItem(ctx, itemID); err != nil {return err}// Шаг 2: назначение исполнителяif err := assignExecutor(ctx, orderID, executorID); err != nil {// компенсация_ = cancelReserve(ctx, itemID)return err}return nil}
Как отвечать на собеседовании
Начни с уточнения: "Вопрос про консистентность между резервом и назначением исполнителя". Затем опиши два сценария: монолит (одна БД) и микросервисы. Для монолита - транзакция с FOR UPDATE, подчеркни важность коротких транзакций и отсутствия внешних вызовов внутри. Для микросервисов - Saga или Outbox, объясни trade-off: строгая консистентность vs доступность. Упомяни TTL для мягкого резерва как защиту от "зависших" заказов. Если спросят про Go - скажи про pgx, database/sql, context для отмены операций. Не углубляйся в детали реализации, пока не попросят.
Что проверяет интервьюер
- Понимание атомарности и изоляции транзакций.
- Умение выбирать между локальной и распределённой транзакцией.
- Знание паттернов Saga, Outbox, компенсации.
- Понимание trade-off между консистентностью и производительностью.
- Практические навыки работы с БД в Go (блокировки, транзакции).
- Умение рассуждать об отказах и восстановлении (retry, идемпотентность).
Типичные ошибки
- Предложение "просто сделать два запроса" без транзакции - не гарантирует атомарность.
- Использование
SELECTбезFOR UPDATE- гонки при конкурентном доступе. - Внешний HTTP-вызов внутри транзакции - долгая блокировка, риск deadlock.
- Игнорирование таймаутов и отмены контекста в Go - зависшие транзакции.
- Отсутствие компенсации в Saga - при сбое на втором шаге резерв остаётся навсегда.
- Неучёт идемпотентности при retry - дублирование заказов или исполнителей.
> Похожие задачи по backend
Как обрабатывать ошибки при отмене брони на складе
Как снять бронь товара при отмене заказа
Напиши функцию на Go, которая принимает слайс каналов int и возвращает канал с суммами чисел из каждого канала
Как реализовать эффективную пагинацию с курсорами и индексами
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью