> Как изменить взаимодействие между сервисами, чтобы избежать дублирования заказов при ретраях (PHP)
Уровень: senior · Роль: backend · Язык: PHP · Категория: Технические вопросы
Компании: Travelata
Стек: PHP
> Пример ответа
Короткий ответ
Основная проблема - идемпотентность: повторная обработка одного и того же события не должна создавать новый заказ. Решение - добавить ключ идемпотентности (например, X-Idempotency-Key или UUID события) на уровне API, проверять его в БД через уникальный индекс и возвращать уже созданный заказ при повторном запросе. Дополнительно - использовать outbox pattern для атомарной записи события и заказа, а также корректно обрабатывать ретраи на стороне клиента с экспоненциальной задержкой.
Подробное объяснение
Дублирование заказов при ретраях возникает из-за отсутствия гарантии, что операция выполнится ровно один раз. Ретраи могут происходить на разных уровнях: сетевые таймауты, повторная отправка от клиента, повторная доставка из очереди. Каждый уровень требует своего подхода.
Ключевая идея - идемпотентность. Сервис должен уметь отличать "новый запрос" от "повторной попытки того же запроса". Для этого:
-
Клиент генерирует уникальный ключ (UUID) для каждой логической операции (создание заказа). Ключ передаётся в заголовке или теле запроса.
-
Сервис проверяет ключ: если запись с таким ключом уже существует - возвращает ранее созданный результат, не выполняя операцию повторно.
-
Уникальный индекс в БД на колонке
idempotency_keyгарантирует, что даже при конкурентных запросах только один пройдёт, остальные получат ошибку или существующий результат. -
Outbox pattern: если создание заказа и отправка события в очередь происходят в одной транзакции, то при сбое после коммита, но до отправки, событие не потеряется - его подхватит отдельный процесс из таблицы outbox.
-
Обработка ретраев в очереди: consumer должен использовать тот же ключ идемпотентности, чтобы повторная доставка сообщения не привела к дублированию.
Важно понимать trade-off: идемпотентность требует дополнительного хранения ключей и проверок, но это стандартная практика для распределённых систем.
На практике
В PHP-проектах (Symfony, Laravel) это реализуется через middleware или event listener. Для очередей (RabbitMQ, Kafka) - через ключ сообщения. Для API - через заголовок Idempotency-Key.
Типичный сценарий:
- Клиент отправляет POST
/ordersсIdempotency-Key: uuid. - Сервис проверяет наличие записи в таблице
order_idempotencyпо ключу. - Если нет - создаёт заказ и записывает ключ в той же транзакции.
- Если есть - возвращает существующий заказ (HTTP 200 или 201 с тем же телом).
Для очередей: сообщение содержит event_id, consumer проверяет его в таблице processed_events. Если уже обработан - просто подтверждает (ack) и не выполняет логику.
Пример кода
PHP// Контроллер в Symfonypublic function createOrder(Request $request): JsonResponse{$idempotencyKey = $request->headers->get('Idempotency-Key');if (!$idempotencyKey) {return new JsonResponse(['error' => 'Idempotency-Key required'], 400);}$existingOrder = $this->orderRepository->findByIdempotencyKey($idempotencyKey);if ($existingOrder) {return $this->json($existingOrder, 200);}try {$order = $this->orderService->createOrder($request->toArray(), $idempotencyKey);return $this->json($order, 201);} catch (UniqueConstraintViolationException $e) {// Конкурентный запрос - возвращаем существующий$order = $this->orderRepository->findByIdempotencyKey($idempotencyKey);return $this->json($order, 200);}}// Сервис с транзакциейpublic function createOrder(array $data, string $key): Order{return $this->entityManager->transactional(function () use ($data, $key) {$order = new Order($data);$this->entityManager->persist($order);$idempotencyRecord = new IdempotencyRecord($key, $order->getId());$this->entityManager->persist($idempotencyRecord);$this->entityManager->flush();return $order;});}
Как отвечать на собеседовании
Начните с определения проблемы: "Ретраи ломают идемпотентность". Затем предложите решение через ключ идемпотентности и уникальный индекс. Упомяните outbox pattern для гарантии доставки событий. Объясните, как обрабатывать конкурентные запросы (catch unique violation). Если спросят про очереди - расскажите про дедупликацию на стороне consumer. Завершите trade-off: хранение ключей, срок жизни записей, очистка старых ключей.
Что проверяет интервьюер
- Понимание идемпотентности и её важности в распределённых системах.
- Умение проектировать API с учётом ретраев.
- Знание паттернов: idempotency key, outbox, deduplication.
- Понимание конкурентного доступа и работы с уникальными индексами.
- Практический опыт с PHP-фреймворками и очередями.
Типичные ошибки
- Предложение "просто проверять наличие заказа по данным" - не работает, так как данные могут совпадать у разных заказов.
- Игнорирование конкурентных запросов - без уникального индекса два параллельных запроса создадут два заказа.
- Отсутствие обработки ошибки уникальности - нужно ловить исключение и возвращать существующий результат.
- Хранение ключей бессрочно - нужно TTL или периодическая очистка.
- Неучёт ретраев на уровне очереди - consumer должен быть идемпотентным, а не только API.
> Похожие задачи по PHP
Какие принципы проектирования кроме SOLID существуют
С какими паттернами проектирования ты работал
Как изменить взаимодействие с внешним сервисом, чтобы избежать увеличения нагрузки из-за ретраев
Напишите функцию, которая асинхронно сделает fetch-запрос, распарсит ответ и вернет имя пользователя
> Похожие задачи по backend
Какие принципы проектирования кроме SOLID существуют
С какими паттернами проектирования ты работал
Как изменить взаимодействие с внешним сервисом, чтобы избежать увеличения нагрузки из-за ретраев
Напишите функцию, которая асинхронно сделает fetch-запрос, распарсит ответ и вернет имя пользователя
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью