> Как определить заказ для повторного запроса с несколькими попытками (PHP)

Уровень: senior · Роль: backend · Язык: PHP · Категория: Технические вопросы

Компании: Travelata

Стек: PHP

> Пример ответа

Короткий ответ

Для повторного запроса с несколькими попытками заказ определяется через уникальный идентификатор идемпотентности (idempotency key), который генерируется клиентом перед первой отправкой и передаётся в каждом запросе. Сервер сохраняет этот ключ вместе с результатом обработки, и при повторной попытке с тем же ключом возвращает сохранённый ответ, не выполняя операцию заново. Это стандартный паттерн для распределённых систем, где сетевые сбои и таймауты неизбежны.

Подробное объяснение

Проблема повторных запросов возникает, когда клиент не получил ответ из-за сетевого сбоя, таймаута или ошибки сервера, и вынужден повторить запрос. Без механизма идемпотентности повторная отправка может привести к дублированию операции - например, двойному списанию средств или созданию двух заказов.

Решение - идемпотентный ключ (idempotency key). Это строка, которую клиент генерирует один раз для логической операции и передаёт в заголовке (например, Idempotency-Key) или в теле запроса. Сервер обязан:

  1. Проверить, существует ли уже запись с этим ключом.
  2. Если записи нет - выполнить операцию, сохранить ключ и результат (статус, тело ответа) в хранилище (БД, Redis).
  3. Если запись есть - вернуть сохранённый результат, не выполняя операцию.

Ключ должен быть уникальным для каждой операции, но одинаковым для всех попыток одной операции. Обычно это UUID v4 или строка, сгенерированная на основе параметров запроса и времени.

Важный нюанс - атомарность проверки и сохранения. Если два параллельных запроса с одним ключом придут одновременно, оба могут не найти запись и выполнить операцию дважды. Решается через уникальный индекс на ключ в БД или атомарную операцию SETNX в Redis.

Срок жизни ключа ограничен (например, 24 часа), после чего запись можно удалять. Это защищает хранилище от переполнения.

На практике

В PHP с типичным стеком (MySQL + Redis) реализация выглядит так:

  • Клиент генерирует ключ и передаёт его в заголовке.
  • Middleware или контроллер проверяет наличие ключа в Redis через SET с опцией NX и TTL.
  • Если ключ новый - выполняется бизнес-логика, результат сериализуется и сохраняется в Redis.
  • Если ключ существует - возвращается сохранённый ответ.

Для MySQL используется таблица с уникальным индексом на колонку ключа. Вставка выполняется через INSERT ... ON DUPLICATE KEY UPDATE или с предварительной проверкой в транзакции с уровнем изоляции SERIALIZABLE.

Ключевой момент - результат должен сохраняться вместе с HTTP-статусом и заголовками, чтобы повторный ответ был полностью идентичен первому.

Пример кода

PHP
<?php
use Redis;
use RuntimeException;
class IdempotentHandler
{
public function __construct(
private Redis $redis,
private int $ttl = 86400
) {}
public function handle(string $key, callable $operation): array
{
// Атомарно резервируем ключ
$reserved = $this->redis->set(
"idempotency:{$key}",
'processing',
['NX', 'EX' => $this->ttl]
);
if (!$reserved) {
$cached = $this->redis->get("idempotency:{$key}");
if ($cached === 'processing') {
// Запрос ещё выполняется - можно вернуть 409 или подождать
throw new RuntimeException('Request already in progress');
}
return json_decode($cached, true);
}
try {
$result = $operation();
$payload = json_encode([
'status' => 200,
'body' => $result,
]);
$this->redis->set("idempotency:{$key}", $payload, ['EX' => $this->ttl]);
return $result;
} catch (\Throwable $e) {
// Удаляем ключ, чтобы клиент мог повторить с тем же ключом
$this->redis->del("idempotency:{$key}");
throw $e;
}
}
}
// Использование
$handler = new IdempotentHandler($redis);
$key = $_SERVER['HTTP_IDEMPOTENCY_KEY'] ?? null;
if (!$key) {
http_response_code(400);
exit('Missing Idempotency-Key header');
}
$response = $handler->handle($key, function () {
// Создание заказа, списание средств и т.д.
return ['order_id' => 12345];
});
echo json_encode($response);

Как отвечать на собеседовании

Начни с определения проблемы: повторные запросы возникают из-за сетевых сбоев, и без идемпотентности операция выполняется дважды. Затем объясни механизм: клиент генерирует ключ, сервер сохраняет результат, при повторе возвращает сохранённое. Подчеркни важность атомарности при первом сохранении - это главный подводный камень. Упомяни TTL и очистку старых ключей. Если спросят про параллельные запросы - расскажи про SETNX или уникальный индекс в БД. Хорошо добавить пример с Redis, так как это стандартное решение для PHP.

Что проверяет интервьюер

Интервьюер оценивает понимание распределённых систем и сетевых сбоев. Ключевые пункты: знание паттерна идемпотентности, умение объяснить проблему гонки (race condition) и способы её решения, понимание trade-off между хранением результата и временем жизни ключа. Также проверяется способность применить паттерн к конкретному стеку - PHP + Redis или MySQL. Важно показать, что ты думаешь не только о happy path, но и об ошибках: что делать при сбое во время выполнения операции, как обрабатывать конфликт параллельных запросов.

Типичные ошибки

  • Использование самого запроса или его параметров как ключа - это не гарантирует уникальность операции, так как одинаковые параметры могут быть у разных логических операций.
  • Игнорирование атомарности при первой проверке - два параллельных запроса оба выполнят операцию.
  • Не сохранять HTTP-статус и заголовки ответа - повторный ответ будет неполным.
  • Хранить ключи бессрочно - это приводит к переполнению хранилища.
  • Удалять ключ при ошибке выполнения - тогда клиент не сможет повторить с тем же ключом, и операция может выполниться дважды при следующей попытке.
  • Не обрабатывать случай, когда запрос с ключом приходит, а операция ещё выполняется - нужен либо статус 409, либо ожидание завершения.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью