Коротко: идемпотентность — операция даёт один результат при любом числе повторов; для API критична, иначе повторный запрос спишет деньги дважды.
Идемпотентность — это свойство операции давать один и тот же результат, сколько бы раз её ни повторили. Для API это критично: сети ненадёжны, клиенты ретраят запросы, и без идемпотентности повторный «создать платёж» легко спишет деньги дважды. Разберём, какие методы идемпотентны по природе, и как сделать идемпотентным то, что им не является.
Почему это важно
Классический сценарий: клиент отправил POST /payments, сервер обработал и списал деньги, но ответ потерялся в сети (таймаут). Клиент не знает, прошёл ли запрос, и повторяет его. Без защиты — второе списание. С идемпотентностью — повтор вернёт результат первого запроса, не создавая дубль.
Идемпотентность HTTP-методов
- GET, HEAD — идемпотентны и безопасны (ничего не меняют).
- PUT — идемпотентен: «положить ресурс в состояние X» повторно даёт то же состояние.
- DELETE — идемпотентен: удалить уже удалённое — состояние то же (обычно 404/204).
- POST — не идемпотентен по умолчанию: каждый вызов создаёт новый ресурс. Именно его и нужно защищать.
- PATCH — зависит от семантики (инкремент
+1не идемпотентен, установка поля — идемпотентна).
Idempotency-Key — стандартный приём
Клиент генерирует уникальный ключ на каждую логическую операцию и шлёт его в заголовке. Сервер запоминает ключ и результат; при повторе с тем же ключом возвращает сохранённый ответ, не выполняя операцию снова.
POST /payments
Idempotency-Key: 5f3c9a1e-...-clientgenerated
Content-Type: application/json
{ "amount": 1000, "currency": "RUB" }
Так делают Stripe, платёжные шлюзы и зрелые API. Ключ обычно живёт ограниченное время (например, 24 часа).
Серверная логика обработки ключа
public function createPayment(string $idempotencyKey, array $data): Response
{
// 1) уже видели такой ключ? вернуть сохранённый результат
if ($saved = $this->store->find($idempotencyKey)) {
return $saved->response;
}
// 2) атомарно «застолбить» ключ (уникальный индекс в БД)
// при гонке двух одинаковых запросов второй упрётся в дубликат
try {
$this->store->reserve($idempotencyKey);
} catch (DuplicateKeyException) {
return $this->waitOrReturnInProgress($idempotencyKey);
}
// 3) выполнить операцию и сохранить результат под ключом
$result = $this->processPayment($data);
$this->store->save($idempotencyKey, $result);
return $result;
}
Ключевой момент — атомарность: «застолбить» ключ нужно через уникальный индекс или блокировку, иначе два одновременных повтора оба пройдут проверку шага 1.
Где ещё нужна идемпотентность
- Очереди сообщений (Kafka/RabbitMQ) — доставка «at least once» означает, что сообщение может прийти дважды. Обработчик обязан быть идемпотентным (по message-id или бизнес-ключу).
- Вебхуки — внешние сервисы ретраят доставку; обрабатывайте по event-id.
- Фоновые задачи — воркер может упасть и перезапустить задачу.
Приёмы обеспечения идемпотентности
- Idempotency-Key + хранилище результатов (описано выше).
- Естественный уникальный ключ: уникальный индекс на бизнес-поле (например, номер заказа) — повторный INSERT упрётся в дубликат.
- Upsert (
INSERT ... ON DUPLICATE KEY UPDATE/ON CONFLICT) вместо «проверил-вставил». - Проверка состояния: «если платёж уже в статусе paid — ничего не делать».
Вывод
Идемпотентность — не «фича для красоты», а базовое требование к надёжному API и обработчикам сообщений. Любую операцию, которую клиент может повторить (а он повторит), нужно проектировать так, чтобы повтор не навредил. Дешевле заложить это сразу, чем ловить двойные списания в проде.
Если проектируете платёжное или интеграционное API и хотите заложить идемпотентность и обработку повторов правильно — разберём на консультации.


Комментарии