Слогер Создать блог
Разработка

Как избежать двойного списания в REST API: идемпотентность на практике

Когда ответ на платежный запрос теряется, повторная попытка приводит к второму списанию. Объясняем, как идемпотентный ключ решает эту проблему и что учесть при реализации.

Клиент нажал «Оплатить», деньги списались, но ответ потерялся. Браузер повторил запрос — и списалось еще раз. Ситуация до боли знакома всем, кто сталкивался с платежными API. И дело не в плохом провайдере, а в том, что сервер не отличает повторное сообщение от нового. Разбираем, как это исправить с помощью идемпотентности.

Почему повторный запрос становится вторым платежом

Представьте: пользователь жмет кнопку оплаты, сервер отправляет команду провайдеру, деньги списываются. Но соединение обрывается раньше, чем ответ доходит до браузера. Браузер видит таймаут и считает, что запрос не удался. Он шлет тот же POST еще раз. Сервер принимает его как новую операцию и инициирует второе списание. В результате у клиента две транзакции.

Каждый компонент по отдельности работает правильно. Браузер ретраит, потому что не получил ответ. API обрабатывает валидный POST. Провайдер выполняет команду. Проблема — в отсутствии контекста, который позволил бы серверу понять: второй запрос — это ретрай первого.

Что такое идемпотентность

Идемпотентность — это свойство операции: сколько раз ее ни повторяй, результат будет одинаковым. Для API платежей это означает, что даже при нескольких HTTP-запросах клиент платит только один раз.

Чтобы это реализовать, нужен идемпотентный ключ. Это уникальная строка, которую клиент генерирует для одной логической операции. Например, для одного чека. Ключ передается в заголовке, обычно Idempotency-Key, и остается одинаковым для всех попыток доставки этого платежа. Новая операция — новый ключ.

Как устроена идемпотентная ручка

Просто сохранить список использованных ключей недостаточно. Нужно хранить состояние операции и ответ, который отдавался при первом обращении. Иначе повторный запрос получит «рыбу», а не реальный результат.

Поле в БДЗачем
idempotency_keyСам ключ, первичный ключ таблицы
request_hashХеш значимых полей запроса — чтобы ключ нельзя было переиспользовать с другим телом
statusprocessing, completed, rejected или recovery_required
response_statusHTTP-статус, который вернется при повторе
response_bodyТело ответа первого запроса

Главная ошибка: проверка вместо атомарной вставки

Типичный небезопасный паттерн выглядит так: сначала проверить, есть ли ключ в базе. Если нет — создать платеж, потом сохранить ключ. Звучит логично, но при параллельных запросах оба «видят» отсутствие ключа и оба создают платеж. Уже после двух списаний выясняется, что уникальный индекс не дал вставить вторую запись, но отменить платеж провайдеру он не может.

Правильный подход — резервировать ключ до начала любых побочных эффектов, используя атомарную операцию. Например, в PostgreSQL это делается вставкой с ON CONFLICT DO NOTHING и возвратом строки:

INSERT INTO idempotency_records (idempotency_key, request_hash, status, expires_at) VALUES ($1, $2, 'processing', NOW() + INTERVAL '24 hours') ON CONFLICT (idempotency_key) DO NOTHING RETURNING idempotency_key;

Только один запрос получит строку в результате. Он и становится владельцем операции. Все остальные должны прочитать уже существующую запись и решить: вернуть сохраненный ответ, если операция завершена, или сообщить о конфликте, если она еще выполняется.

Как обрабатывать повторные запросы

  1. Клиент генерирует UUID и отправляет его в заголовке Idempotency-Key.
  2. Сервер вычисляет хеш тела запроса по фиксированной структуре (например, JSON.stringify с определенным порядком полей).
  3. Вставляет запись со статусом processing.
  4. Если получилось — эта попытка первая, можно вызывать провайдера.
  5. Если не получилось — читаем существующую запись. Сравниваем хеш: не совпал — возвращаем 422, совпал и статус completed — отдаем сохраненный ответ, статус processing — возвращаем 409.
  6. После успешного ответа от провайдера обновляем запись: status='completed', сохраняем код и тело ответа.
  7. Если провайдер вернул ошибку, помечаем запись как recovery_required — нужно свериться с провайдером перед тем, как разрешить повтор.

Почему локальной базы недостаточно

Самый сложный случай — когда провайдер списал деньги, а приложение упало до обновления записи в БД. Локальная транзакция не поможет: провайдер и ваша база не делят одну границу транзакции. Нельзя гарантировать, что успех в одном месте означает успех в другом.

Поэтому идемпотентный ключ нужно передавать и провайдеру, если его API это поддерживает. Тогда повторная попытка с тем же ключом не создаст вторую операцию. Если провайдер такого не умеет, придется как можно раньше сохранять его идентификатор операции и через фоновый воркер сверять реальное состояние перед повтором.

Итог

Двойное списание — это не баг клиента и не шалость провайдера. Это закономерный результат того, что API не различает транспортный запрос и бизнес-операцию. Ретраи неизбежны, так что без идемпотентности любая потеря пакета может обернуться финансовым инцидентом.

Реализация не сводится к одному middleware. Нужны атомарное резервирование ключа, хранение ответа, проверка хеша и координация с провайдером. Зато после этого клиент может повторять запрос сколько угодно — и заплатит ровно один раз.

По материалам: dev. Текст переработан редакцией Слогера.

← На главную

Рекламное место — Конец поста
Реклама · Слогер

Комментарии (25)

Войдите, чтобы комментировать.

Md.Alomgir Hossain 29.08.2026 13:30 0
Отличная и очень познавательная
Hasan1122 29.08.2026 13:34 0
Вот отличный вариант комментария на русском языке для публикации на Sloger: ​Спасибо за отличную и действительно полезную статью! 👏 ​Проблема двойных списаний при интеграции платежных шлюзов — действительно очень частая, но критическая ошибка. Многие разработчики на первых порах пытаются обработать ее обычной проверкой через БД и попадают в ловушку race condition. ​Особенно точно вы подсветили темы с атомарной вставкой (ON CONFLICT DO NOTHING) и проверкой хэша тела запроса (Request Body Hash). Ключ может совпадать, но содержимое тела измениться, поэтому проверка хэша крайне важна. ​Отдельная благодарность автору за то, что объяснил тему идемпотентности на бэкенде простым языком и с понятными примерами!
Hassan Mohamed Hussein 29.08.2026 13:43 0
Очень полезная и понятная статья! Особенно понравилось практическое объяснение идемпотентности в REST API и способов избежать двойного списания. Материал изложен доступно, с акцентом на реальные сценарии и важные детали реализации.
Hassan Mohamed Hussein 29.08.2026 13:44 0
Очень полезная и понятная статья! Особенно понравилось практическое объяснение идемпотентности в REST API и способов избежать двойного списания. Материал изложен доступно, с акцентом на реальные сценарии и важные детали реализации.
Hassan Mohamed Hussein 29.08.2026 13:44 0
Очень полезная и понятная статья! Особенно понравилось практическое объяснение идемпотентности в REST API и способов избежать двойного списания. Материал изложен доступно, с акцентом на реальные сценарии и важные детали реализации.
Fluxs comment
Реклама · fluxs.ru
Mohammid Haider 29.08.2026 16:22 0
Very nice post.
Mohammid Haider 29.08.2026 16:22 0
Very nice post.
Mohammid Haider 29.08.2026 16:23 0
Very nice post
Mohammid Haider 29.08.2026 16:25 0
Nice Artical
Mohammid Haider 29.08.2026 16:26 0
Nice article
Fluxs comment
Реклама · fluxs.ru
Maksudul Apon 29.08.2026 16:46 0
Это чрезвычайно важно и полезно; материал затрагивает серьезную проблему — особенно в сфере заработка в интернете, — которая зачастую приводит к значительным трудностям. Спасибо за эту важную публикацию!
Maksudul Apon 29.08.2026 16:50 0
Это отличная и важная информация — особенно для тех из нас, кто зарабатывает в интернете. Спасибо за публикацию.
Channel Bangla Media 29.08.2026 17:14 0
Это очень практичная и хорошо объясненная статья. Обсуждение резервирования атомарных ключей, проверки хеша запроса и идемпотентности на стороне поставщика наглядно показывает, почему предотвращение дублирующих платежей требует большего, чем простая проверка базы данных. Отличные идеи для всех, кто разрабатывает надежные платежные API.
Channel Bangla Media 29.08.2026 17:14 0
Это очень практичная и хорошо объясненная статья. Обсуждение резервирования атомарных ключей, проверки хеша запроса и идемпотентности на стороне поставщика наглядно показывает, почему предотвращение дублирующих платежей требует большего, чем простая проверка базы данных. Отличные идеи для всех, кто разрабатывает надежные платежные API.
aniyaawan593 29.08.2026 18:05 0
Очень полезная и понятная статья! Мне особенно понравилось практическое объяснение идемпотентности в REST API и способы предотвращения двойной траты средств. Материал изложен ясно, с акцентом на реальные сценарии и важные детали реализации.
Fluxs comment
Реклама · fluxs.ru
aniyaawan593 29.08.2026 18:06 0
Очень полезная и понятная статья! Мне особенно понравилось практическое объяснение идемпотентности в REST API и способы предотвращения двойной траты средств. Материал изложен ясно, с акцентом на реальные сценарии и важные детали реализации.
aniyaawan593 29.08.2026 18:07 0
Очень полезная и понятная статья! Мне особенно понравилось практическое объяснение идемпотентности в REST API и способы предотвращения двойной траты средств. Материал изложен ясно, с акцентом на реальные сценарии и важные детали реализации.
atgasapt 29.08.2026 18:10 0
Отличная статья! 👍 Очень важно понимать принцип идемпотентности в REST API, особенно в контексте платежных систем. Повторные запросы без идемпотентного ключа действительно могут привести к двойному списанию, что критично для пользователей и бизнеса.
atgasapt 29.08.2026 18:10 0
Отличная статья! 👍 Очень важно понимать принцип идемпотентности в REST API, особенно в контексте платежных систем. Повторные запросы без идемпотентного ключа действительно могут привести к двойному списанию, что критично для пользователей и бизнеса.
Sofia Dmitry 29.08.2026 21:10 0
Очень интересная и полезная статья! Особенно понравилось подробное объяснение того, как идемпотентность помогает избежать повторных платежей при сбоях связи. На мой взгляд, проверка хеша запроса и сохранение результата операции действительно являются важными деталями надежной реализации. Спасибо за понятное и практичное объяснение сложной темы
Fluxs comment
Реклама · fluxs.ru
Sofia Dmitry 29.08.2026 21:10 0
Очень интересная и полезная статья! Особенно понравилось подробное объяснение того, как идемпотентность помогает избежать повторных платежей при сбоях связи. На мой взгляд, проверка хеша запроса и сохранение результата операции действительно являются важными деталями надежной реализации. Спасибо за понятное и практичное объяснение сложной темы
Sofia Dmitry 29.08.2026 21:10 0
Очень интересная и полезная статья! Особенно понравилось подробное объяснение того, как идемпотентность помогает избежать повторных платежей при сбоях связи. На мой взгляд, проверка хеша запроса и сохранение результата операции действительно являются важными деталями надежной реализации. Спасибо за понятное и практичное объяснение сложной темы
Sofia Dmitry 29.08.2026 21:11 0
Очень интересная и полезная статья! Особенно понравилось подробное объяснение того, как идемпотентность помогает избежать повторных платежей при сбоях связи. На мой взгляд, проверка хеша запроса и сохранение результата операции действительно являются важными деталями надежной реализации. Спасибо за понятное и практичное объяснение сложной темы
Sofia Dmitry 29.08.2026 21:11 0
Очень интересная и полезная статья! Особенно понравилось подробное объяснение того, как идемпотентность помогает избежать повторных платежей при сбоях связи. На мой взгляд, проверка хеша запроса и сохранение результата операции действительно являются важными деталями надежной реализации. Спасибо за понятное и практичное объяснение сложной темы
Zuhaib 29.08.2026 22:54 0
Очень подробно раскрыта тема идемпотентности, особенно примеры с заголовками idempotentnosty-key. Спасибо!