🔁 Go: как не создать два заказа одним запросом
Представим ситуацию.
Клиент отправляет запрос на создание заказа. Сервер успешно создаёт заказ, но ответ теряется изза сетевого сбоя.
Клиент не знает, выполнилась операция или нет, поэтому отправляет запрос повторно.
И вот у нас уже два заказа вместо одного.
Это не какая-то экзотическая ситуация. Повторные запросы возникают из-за таймаутов, сетевых ошибок, повторных попыток на стороне клиента и особенностей работы мобильных приложений.
Один из способов решить эту проблему, сложно произносимое слово - идемпотентность (Idempotency).
Решил закрепить тему на практике и написал небольшой пример на go. Как это работает?
Клиент отправляет запрос с уникальным заголовком:
POST /orders Idempotency-Key: checkout-123 Content-Type: application/json { "product_id": 42, "quantity": 1 }
При первом запросе сервер создаёт заказ и сохраняет результат операции вместе с ключом.
Если клиент повторит запрос с тем же ключом и теми же данными, сервер вернёт первоначальный результат, а не создаст новый заказ.
Если же повторно использовать ключ с другим содержимым, сервер вернёт 409 Conflict.
А что насчёт конкурентных запросов?
Просто проверить, существует ли ключ, недостаточно.
Два одинаковых запроса могут прийти одновременно. Оба проверят хранилище, не найдут ключ и попытаются создать заказ.
Поэтому важно не только хранить ключи, но и правильно координировать параллельные операции.
В примере я показал этот сценарий и добавил тесты, которые проверяют, что параллельные запросы с одним ключом не вызывают создание нескольких заказов. Можно ли использовать такой подход в проде?
Сам принцип - да. Но текущий пример хранит данные в памяти одного процесса. После перезапуска они исчезнут, а несколько экземпляров сервиса не будут делить общее хранилище.
Для production потребуются постоянное хранилище, политика хранения ключей, защита от гонок на уровне базы данных и согласованность записи ключа с созданием заказа.
Главная мысль: дернуть ручку повторно не обязательно означает, что бизнес-операцию нужно выполнять заново. Надёжный апи должен учитывать сетевые сбои и повторные попытки клиента.
Код с инструкцией запуска и тестами выложил на гитхаб: https://github.com/wolverineo250kr/go-idempotency-demo
Продолжаю разбирать практические задачи go бэк-разработки. В этот раз - не просто реализация эндпоинта поведение системы в ситуации, которая вполне может возникнуть на реальном проде.