Что такое идемпотентность
Что проверяют
Отличаете ли вы идемпотентность от безопасности HTTP-методов и умеете ли сделать устойчивой к повторам операцию, которая по природе неидемпотентна, например оплату. Следом обычно спрашивают, как сделать ключ идемпотентности в ASP.NET Core.
Как отвечать
Операция идемпотентна, если повторное выполнение с теми же параметрами оставляет систему в том же состоянии, что и однократное. Речь о состоянии, а не об ответе: второй DELETE /orders/5 вернёт 404, но операция остаётся идемпотентной.
По RFC 9110:
- безопасные (
GET,HEAD,OPTIONS): не меняют состояние, значит, и идемпотентны; - идемпотентные, но не безопасные:
PUTиDELETE; - неидемпотентные:
POST, а такжеPATCHв общем случае, всё зависит от формата изменения.
Сеть теряет ответы, и клиент повторяет запрос вслепую. В .NET ретраи обычно ставят через Polly или Microsoft.Extensions.Http.Resilience, и стандартный обработчик по умолчанию повторяет запросы любых методов. Для POST на оплату это двойное списание, поэтому ретраи небезопасных методов отключают явно (DisableForUnsafeHttpMethods()) или делают сам POST идемпотентным:
- ключ идемпотентности: клиент генерирует GUID и отправляет в заголовке
Idempotency-Key. Сервер сохраняет ключ, хеш тела и ответ, а повтор с тем же ключом получает сохранённый ответ. В ASP.NET Core это middleware или фильтр действия; - уникальный бизнес-ключ в базе: повторная вставка падает с
DbUpdateException, её ловят и возвращают уже созданную запись; - обработчики сообщений: RabbitMQ и Kafka доставляют как минимум один раз, поэтому потребитель хранит обработанные
MessageId(в MassTransit для этого есть inbox).
Где спотыкаются
Путают идемпотентность с безопасностью и говорят, что идемпотентный запрос вообще не меняет данные.
Проверяют наличие ключа и записывают его отдельными шагами. Два параллельных запроса оба проходят проверку. Нужна атомарная операция: уникальный индекс или SET NX в Redis.
Не связывают ключ с телом запроса. Повтор с тем же ключом и другой суммой должен получать ошибку, а не старый ответ. А операция Balance = Balance - @amount не идемпотентна, в отличие от Status = 'Paid'.