Короткий ответ
Клиент передаёт уникальный идемпотентный ключ (idempotency key) на операцию, а сервер при получении запроса с уже обработанным ключом возвращает сохранённый результат первой обработки вместо повторного выполнения бизнес-логики.
Как это работает подробнее
Сервер хранит таблицу «idempotency_key → статус и результат операции»; при поступлении запроса сначала атомарно проверяется и резервируется ключ (например, через уникальный индекс в БД с транзакцией), и если ключ уже существует и операция завершена — возвращается сохранённый ответ без повторного списания.
Пример кода
query.sql
CREATE UNIQUE INDEX idx_idempotency_key ON payments (idempotency_key);
-- атомарная попытка зарезервировать ключ:
INSERT INTO payments (idempotency_key, status, amount)
VALUES ('idem-key-123', 'processing', 1000)
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING id;
-- если строк не вернулось -- значит ключ уже обрабатывается/обработан,
-- нужно отдать сохранённый ранее результат, а не списывать средства повторноЧто использовать на практике
- Практика. Ключ идемпотентности должен генерироваться клиентом один раз на логическую попытку операции (а не на каждый HTTP-retry) и быть достаточно уникальным (UUID); время жизни записи об идемпотентности нужно ограничивать разумным сроком, достаточным для покрытия ретраев, но не бесконечным.
Подводные камни
- Риск. Реализуют идемпотентность только «по смыслу» (проверяют, нет ли уже такого заказа по сумме и времени), что ненадёжно при параллельных запросах; не защищают саму проверку и резервирование ключа от гонки между двумя одновременными запросами с одним ключом, из-за чего обе обработки всё равно проходят.