Как ответить
Идемпотентность — это свойство операции, при котором повторный вызов с теми же входными данными не меняет состояние системы и возвращает тот же результат, что и первый успешный запрос. На практике это значит, что если клиент отправил запрос на создание транзакции, произошёл сетевой сбой, и клиент повторил запрос, в базе не должно появиться два дубликата операции.
В контексте HTTP-методов по спецификации RFC 9110 идемпотентными считаются GET, HEAD, OPTIONS, PUT и DELETE. Метод POST по умолчанию не идемпотентен, так как каждый вызов обычно создает новый ресурс. Однако в распределенных системах мы часто реализуем идемпотентность для POST вручную, чтобы избежать проблем из-за дублей при сетевых задержках или ретраях на стороне клиента.
Основные способы реализации идемпотентности на бэкенде:
- Ключ идемпотентности (Idempotency-Key): Клиент генерирует уникальный UUID для операции и передает его в заголовке. Сервер сохраняет этот ключ в Redis или базе данных на определенное время (например, 24 часа). Если приходит запрос с уже существующим ключом, сервер просто возвращает сохраненный результат предыдущего выполнения, не запуская бизнес-логику повторно.
- Уникальные индексы в БД: На уровне схемы данных создается UNIQUE constraint на поля, которые однозначно идентифицируют сущность (например, связка user_id и external_order_id). Повторная вставка вызовет ошибку, которую сервис обработает как успешный дубль.
- Upsert (Update or Insert): Использование конструкций вроде
INSERT ... ON CONFLICT DO UPDATE. Это делает операцию записи идемпотентной «из коробки», так как итоговое состояние записи будет одинаковым вне зависимости от количества вызовов.
Пример реализации проверки ключа в псевдокоде:
def process_payment(request):
key = request.headers.get('Idempotency-Key')
# Проверяем, обрабатывали ли мы этот запрос ранее
cached_response = cache.get(key)
if cached_response:
return cached_response
# Выполняем бизнес-логику внутри транзакции
with db.transaction():
result = payment_service.charge(request.amount)
# Сохраняем результат, чтобы вернуть его при повторе
cache.set(key, result, expire=86400)
return result
Важно различать идемпотентность и безопасность (Safety). Безопасные методы (GET, HEAD) не меняют состояние сервера вообще. Идемпотентные методы (PUT, DELETE) могут менять состояние, но только один раз при первом вызове. Например, первый DELETE удалит запись и вернет 204, а последующие — 404 или 204 (в зависимости от логики), но запись в БД останется удаленной, состояние не изменится повторно.