Как достичь идемпотентности
Что проверяют
Понимаете ли вы, что повтор запроса неизбежен (таймаут на клиенте, ретрай прокси, повторная доставка из брокера), и умеете ли сделать так, чтобы повтор не дал второго эффекта. Следом спрашивают, что будет, если два одинаковых запроса придут одновременно.
Как отвечать
Операция идемпотентна, если повтор оставляет систему в том же состоянии, что и первый вызов. По RFC 9110 идемпотентны GET, PUT, DELETE, а POST и PATCH нет, их и защищают.
- естественная идемпотентность: переход состояния с условием,
UPDATE orders SET status = 'PAID' WHERE id = ? AND status = 'NEW', и проверка числа изменённых строк. Повтор просто ничего не обновит; - ключ идемпотентности: клиент генерирует UUID один раз на операцию и шлёт его в заголовке
Idempotency-Keyпри каждом повторе. Сервер хранит ключ в таблице с уникальным индексом, статусом и сохранённым ответом; - атомарность: вставка ключа и бизнес-эффект в одной транзакции (
@Transactionalв Spring). Конфликт ловится какDataIntegrityViolationExceptionили черезINSERT ... ON CONFLICT DO NOTHINGв PostgreSQL. Повтор получает сохранённый ответ, а пока первый запрос в работе, отвечают409; - сверка тела: ключ привязывают к хэшу запроса, чтобы тот же ключ с другими данными отклонялся, а не возвращал чужой ответ;
- консьюмеры: Kafka и RabbitMQ доставляют at-least-once, поэтому id обработанных сообщений пишут в таблицу той же транзакцией, что и эффект.
enable.idempotence=trueу продюсера Kafka (включён по умолчанию с 3.0) убирает только дубли от ретраев продюсера, сквозной идемпотентности он не даёт.
Ключи храните с TTL: сутки или неделю, по тому, сколько клиент может повторять запрос.
Где спотыкаются
Делают check-then-act: сначала SELECT ключа, потом вставка. Два параллельных запроса оба не находят ключ и оба выполняют операцию. Гарантию даёт только уникальное ограничение в базе.
Хранят ключи в HashMap внутри сервиса. Второй экземпляр о них не знает, после рестарта всё теряется, а сама HashMap ещё и не потокобезопасна.
Путают идемпотентность с транзакцией. @Transactional откатит неудачную операцию, но успешный повтор выполнит второй раз без вопросов.