Как определить заказ для повторного запроса с несколькими попытками
Что проверяют
Понимаете ли вы, что повтор запроса без идентификатора операции порождает дубли: второй заказ, двойное списание. Интервьюер ждёт слово «идемпотентность», а следом спрашивает, что делать, если ответ на первую попытку просто не пришёл.
Как отвечать
Главная идея: заказ получает идентификатор до первой попытки, и все повторы идут с ним.
- ключ идемпотентности: UUID генерирует инициатор (клиент или ваш сервис перед вызовом внешнего API) и передаёт в заголовке
Idempotency-Keyили полеexternal_id. На принимающей стороне уникальный индекс по нему: повтор не создаёт новую запись, а возвращает результат первой попытки; - статус заказа:
new,processing,paid,failed. Переход делается атомарно:UPDATE ... WHERE id = ? AND status = 'new'и проверка числа затронутых строк; - таймаут не значит отказ: операция могла выполниться. Перед повтором запросите статус по тому же ключу или повторите с ним же, если сервер идемпотентен;
- что повторять: сетевые ошибки, таймауты, 5xx, 429 с учётом
Retry-After. Ошибки 4xx повторять бессмысленно, запрос не станет валиднее; - паузы: экспоненциальный backoff с джиттером и лимит попыток;
- инструменты:
Http::retry()в Laravel,Middleware::retry()в Guzzle, для фона очередь:$triesи$backoffу Laravel job,retry_strategyв Symfony Messenger. После исчерпания попыток заказ уходит в failed jobs и алерт, а не пропадает молча.
sleep() в цикле внутри пользовательского запроса держит воркер PHP-FPM, поэтому долгие повторы переносят в очередь.
Где спотыкаются
Генерируют новый ключ на каждую попытку. Идемпотентности тогда нет, каждый повтор создаёт новый заказ.
Повторяют всё подряд, включая 400 и 422, и забивают логи одинаковыми ошибками.
Проверяют существование заказа через SELECT, а потом делают INSERT. Между ними успевает пройти параллельный повтор, от этого защищает только уникальный индекс в базе.