Ответ
Для предотвращения дублирования запросов можно использовать идемпотентные ключи (idempotency keys). Клиент генерирует уникальный ключ для каждого запроса, сервер сохраняет его и результат обработки. При повторном запросе с тем же ключом возвращается сохранённый результат.
Пример реализации (для демонстрации, в реальных системах используйте внешние хранилища):
var cache = sync.Map{}
func handleRequest(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("Idempotency-Key")
if key == "" {
http.Error(w, "Idempotency-Key header required", http.StatusBadRequest)
return
}
// Попытка загрузить результат из кэша
if val, ok := cache.Load(key); ok {
w.Write(val.([]byte))
return
}
// Обработка запроса (выполняется только если ключ не найден в кэше)
response := []byte("response data for " + key)
cache.Store(key, response)
w.Write(response)
}
Важные аспекты и дополнительные подходы:
- Внешние хранилища: В продакшн-среде
sync.Mapне подходит, так как она не персистентна и не распределена. Используйте Redis, Memcached или базу данных для хранения идемпотентных ключей и результатов. - Время жизни ключей (TTL): Устанавливайте ограниченное время жизни для ключей, чтобы избежать переполнения хранилища и обрабатывать устаревшие запросы как новые.
- Транзакционность: Убедитесь, что сохранение ключа и выполнение операции атомарны или обрабатываются в рамках одной транзакции, чтобы избежать частичных состояний.
- Проверка ключей на уровне базы данных: Для операций, изменяющих данные, можно использовать уникальные ограничения или условные вставки в БД, чтобы предотвратить дублирование на уровне данных.
Ответ 18+ 🔞
Да ты посмотри, какие умные головы придумали! Чтобы один и тот же запрос дважды не обрабатывался, как будто клиент обдолбанный и тыкает кнопку "отправить" до посинения.
Суть проще пареной репы: клиент, прежде чем дернуть API, выдумывает уникальный ключик (idempotency key) — хоть UUID сгенерирует, хоть отпечаток пальца в base64 закодирует. И с каждым запросом этот ключ в заголовке тащит.
А сервер-то не лыком шит! Он этот ключ ловит и думает: "Э, ёпта, а я такого уже не видел?" Если видел — значит, запрос уже обрабатывался, и можно не париться, а просто отдать старый, закешированный ответ. Если не видел — ну тогда работать, блядь, надо, обрабатывать, результат сохранять, а потом уже отдавать.
Вот тебе наглядный пример, как это в коде может выглядеть, на коленке сделанный:
var cache = sync.Map{}
func handleRequest(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("Idempotency-Key")
if key == "" {
http.Error(w, "Idempotency-Key header required", http.StatusBadRequest)
return
}
// Попытка загрузить результат из кэша
if val, ok := cache.Load(key); ok {
w.Write(val.([]byte))
return
}
// Обработка запроса (выполняется только если ключ не найден в кэше)
response := []byte("response data for " + key)
cache.Store(key, response)
w.Write(response)
}
Но это, блядь, учебный пример, для продакшна — полная хуйня! Потому что sync.Map живёт только в памяти одного процесса. Ты его перезапустишь — и все ключи к ебеням, идемпотентность накрылась медным тазом.
Так что в реальной жизни, чувак, делай так:
- Хранилище нормальное бери: Redis, Memcached или даже обычную базу данных. Чтобы ключи пережили перезапуск и были доступны всем инстансам твоего сервиса.
- Срок годности ставь (TTL): А то накопится этих ключей овердохуища, хранилище лопнет. Старый ключ протух — считай запрос новым, обрабатывай заново.
- Либо транзакционно, либо никак: Сохранение ключа и выполнение бизнес-операции (например, списание бабла) должны быть в одной транзакции. Чтобы не получилось так: деньги списались, а ключ не сохранился — и клиент может ещё разок дернуть, думая, что не сработало.
- Базу в дело пускай: Для операций с данными самый надёжный способ — это уникальный constraint в самой базе. Пытаешься вставить запись с уже существующим идемпотентным ключом — база тебе говорит: "Иди нахуй, мудак, это уже было". Вот это надёжно, блядь.