Как идемпотентность API помогает избежать дублирования заказов?
Что проверяют
Понимаете ли вы, откуда берутся дубли: клиент не дождался ответа из-за таймаута и повторил запрос, хотя заказ уже создан. Следом обычно спрашивают, что будет, если два одинаковых запроса придут одновременно.
Как отвечать
Идемпотентная операция при повторе даёт тот же эффект, что и при первом вызове. GET, PUT и DELETE идемпотентны по семантике HTTP, POST нет, а заказ создают именно через POST. Поэтому идемпотентность добавляют явно:
- ключ идемпотентности: клиент генерирует UUID на одну попытку оформления и шлёт его в заголовке
Idempotency-Key, при ретрае ключ тот же; - атомарная фиксация ключа: таблица с уникальным индексом по
(user_id, key)иINSERT ... ON CONFLICT DO NOTHINGчерезpgxилиdatabase/sql, в одной транзакции с созданием заказа; - сохранённый ответ: рядом с ключом храните статус и тело ответа, повтор получает их без повторной работы. Если первый запрос ещё выполняется, отвечайте
409 Conflict; - сверка тела: хеш payload рядом с ключом. Тот же ключ с другим телом означает ошибку клиента, это
422; - TTL: ключи живут ограниченное время, например сутки, потом чистятся фоновой задачей.
В Go это удобно оформить middleware поверх http.Handler, чтобы хендлеры заказов не знали о ключах. Для горячих путей ключ можно захватывать в Redis через SET key value NX EX, но источником истины для заказа остаётся база. И проговорите границы: если внутри вызывается платёжный шлюз, ему тоже передают ключ, иначе заказ будет один, а списаний два.
Где спотыкаются
Проверяют ключ отдельным SELECT перед вставкой. Два параллельных ретрая оба видят «ключа нет» и создают два заказа, спасает только уникальное ограничение в базе.
Генерируют ключ на сервере или заново при каждой попытке. Тогда повтор выглядит как новый запрос, и защита не работает.
Считают, что задачу решает X-Request-ID. Это идентификатор для трассировки, прокси часто выставляет его на каждый запрос заново, для дедупликации он не годится.