Как предотвратить двойной заказ при двойном клике на кнопку?
Что проверяют
Как вы обеспечиваете идемпотентность на бэкенде. Блокировка кнопки тут вторична: запрос может прийти дважды из двух вкладок, из-за ретрая после таймаута или повтора на прокси. Следом спрашивают, что будет, если у сервиса несколько реплик.
Как отвечать
Основа ответа это ключ идемпотентности и уникальное ограничение в базе:
- ключ от клиента: заголовок
Idempotency-Keyс UUID, созданным один раз на попытку оформления. Сервер хранит ключ вместе сuser_id, хешем тела запроса, статусом и сохранённым ответом; - атомарная вставка:
INSERT ... ON CONFLICT (user_id, key) DO NOTHINGи проверкаRowsAffected()уsql.Resultвdatabase/sqlили уpgconn.CommandTagвpgx. Ноль строк значит, что запрос уже был: вернуть сохранённый ответ или409, если первый ещё обрабатывается; - одна транзакция: запись ключа и создание заказа коммитятся вместе, иначе падение между ними оставит ключ без заказа;
- естественная уникальность: заказ заранее создаётся черновиком, а подтверждение это переход состояния
UPDATE orders SET status = 'placed' WHERE id = $1 AND status = 'draft'. Второй запрос обновит ноль строк; - внешние вызовы: тот же ключ передаётся платёжному шлюзу, чтобы деньги не списались дважды.
Ключам задают срок жизни, например сутки, и чистят их фоновой задачей. Если вместо базы Redis, подойдёт SET key value NX EX 86400, но продумайте, что будет при потере данных в Redis.
singleflight из golang.org/x/sync можно упомянуть как склейку одновременных дублей внутри одного процесса, но гарантией он не является.
Где спотыкаются
Ставят глобальный sync.Mutex и флаг в памяти. Это выстраивает в очередь заказы всех пользователей и не работает, когда реплик больше одной.
Делают «проверить, потом вставить» двумя запросами без уникального индекса. Два параллельных запроса оба увидят пустоту и оба создадут заказ.
Режут дубли по времени, не чаще раза в три секунды. Медленный ретрай проходит, а честный второй заказ блокируется.