Короткий ответ
Нужна идемпотентность на основе ключа идемпотентности, атомарно зарезервированного в общем для всех инстансов хранилище (БД с уникальным constraint или распределённый кэш с атомарными операциями) до выполнения операции, плюс чёткая политика, что считается «тем же самым» повторным запросом (точное совпадение тела или только ключ).
Как это работает подробнее
При множественных инстансах приложения локальная память процесса не подходит для хранения состояния идемпотентности — нужно общее хранилище (БД/Redis), и резервирование ключа должно быть атомарным (например, INSERT ... ON CONFLICT DO NOTHING или уникальный индекс), чтобы два почти одновременных повторных запроса, попавших на разные инстансы, не прошли оба как «первые».
Хранение состояния идемпотентности в основной транзакционной БД даёт строгую согласованность с самой бизнес-операцией (резервирование ключа и изменение баланса в одной транзакции), но добавляет нагрузку на БД и требует продуманной схемы очистки старых записей. Использование отдельного быстрого хранилища (Redis) дешевле по нагрузке, но вводит риск рассинхронизации между состоянием идемпотентности и фактическим состоянием бизнес-данных при сбоях между двумя хранилищами. Выбор зависит от критичности операции (финансовая транзакция против менее критичного действия) и приемлемого уровня дополнительной сложности.
Пример кода
-- Атомарное резервирование ключа идемпотентности в той же БД, что бизнес-данные
-- (PostgreSQL)
BEGIN;
INSERT INTO idempotency_keys (key, status, created_at)
VALUES ($1, 'processing', now())
ON CONFLICT (key) DO NOTHING;
-- Если INSERT не вставил строку (конфликт) - запрос уже обрабатывается/обработан,
-- нужно прочитать существующую запись и вернуть её результат, не повторяя списание.
COMMIT;Что использовать на практике
- Практика. Нужно явно решить, что делать, если повторный запрос с тем же ключом идемпотентности пришёл с другим телом запроса (вероятная ошибка клиента или смена сценария) — вернуть ошибку конфликта или обработать как новый запрос; также нужно решить время жизни записи об идемпотентности (слишком короткое — не защитит от редких повторов с большой задержкой, слишком длинное — расходует хранилище и усложняет повторное легитимное использование того же бизнес-сценария).
Подводные камни
- Риск. Реализуют проверку идемпотентности на уровне одного инстанса/процесса без учёта горизонтального масштабирования; обеспечивают идемпотентность только для «счастливого пути» (успешный ответ), но не продумывают, что возвращать, если первая попытка ещё обрабатывается (запрос в процессе) в момент прихода повтора.