Как избежать двойной оплаты при прерванном запросе и повторном выполнении
Что проверяют
Понимаете ли вы, что сеть ненадёжна и повтор запроса неизбежен: если соединение оборвалось после отправки, клиент не знает, прошла ли оплата. Интервьюер ждёт слово «идемпотентность» и её реализацию, а следом обычно спрашивает, что будет, если два одинаковых запроса придут одновременно.
Как отвечать
Каркас: клиент генерирует ключ, сервер гарантирует, что по одному ключу деньги спишутся один раз.
- ключ идемпотентности: клиент создаёт
Guidдо первой попытки и шлёт его в заголовкеIdempotency-Keyпри каждом повторе. Новый ключ на каждую попытку ломает всю схему; - хранение в базе, а не в памяти: таблица с уникальным индексом по ключу, статусом (
Processing,Succeeded,Failed) и сохранённым ответом. Вставка записи и есть захват: второй запрос упрётся в нарушение уникальности (DbUpdateExceptionв EF Core) и дальше не пойдёт; - повтор возвращает тот же результат: если запись в статусе
Succeeded, отдаём сохранённый ответ, а не ошибку. ЕслиProcessing, отвечаем409и просим повторить позже; - прерывание посередине: списание у платёжного провайдера и фиксация статуса у себя в одну транзакцию не укладываются. Поэтому ключ передают дальше провайдеру (платёжные API обычно принимают свой ключ идемпотентности), а зависшие
Processingразбирает фоновый сверщик, запрашивая статус у провайдера; - гонка: проверка «есть ли такой ключ» и запись должны быть атомарны. Это обеспечивает уникальный индекс в базе, а не
ifв коде перед вставкой.
Если оплата идёт через очередь, добавьте паттерн Outbox и дедупликацию у потребителя: брокер с доставкой «хотя бы один раз» повторяет сообщения.
Где спотыкаются
Предлагают HashSet или MemoryCache в памяти процесса. Он теряется при рестарте, не виден второму экземпляру сервиса и без блокировки не потокобезопасен.
Отвечают на повтор ошибкой «уже обработано». Клиент, у которого оборвалась связь, так и не узнает, прошла ли оплата, и пользователь нажмёт кнопку снова.
Ставят отметку об обработке после списания. Если процесс упадёт между списанием и записью, повтор спишет деньги второй раз.