Что такое ключ идемпотентности и как его реализовать в бэкенде?

Ответ

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

Это критически важно для защиты от сбоев в распределенных системах, например:

  • Сетевые ошибки: Клиент отправил запрос, но не получил ответ (например, из-за таймаута). Он не знает, была ли операция выполнена, и безопасно повторяет запрос с тем же ключом.
  • Дублирование сообщений: В системах с очередями сообщений одно и то же событие может быть доставлено несколько раз.
  • Защита от двойных списаний: В платежных системах это предотвращает повторное списание средств при повторной отправке запроса на оплату.

Как это работает:

  1. Клиент генерирует уникальный ключ (например, UUID) и отправляет его в заголовке запроса (например, Idempotency-Key).
  2. Сервер перед выполнением операции проверяет, обрабатывался ли уже запрос с таким ключом.
    • Если ключ новый: Сервер сохраняет ключ, выполняет операцию, сохраняет результат (или статус) и отправляет ответ клиенту.
    • Если ключ уже существует: Сервер не выполняет операцию повторно, а сразу возвращает сохраненный ранее результат.

Пример потокобезопасной реализации в Go HTTP API:

В реальном приложении ключи нужно хранить в персистентном хранилище (Redis, PostgreSQL), но для демонстрации используем потокобезопасную map.

package main

import (
    "fmt"
    "net/http"
    "sync"
)

// В реальном приложении это будет Redis, база данных и т.д.
var ( 
    processedKeys = make(map[string]bool)
    mu            sync.Mutex // Мьютекс для защиты доступа к processedKeys
)

func processPayment(w http.ResponseWriter, r *http.Request) {
    idempotencyKey := r.Header.Get("Idempotency-Key")
    if idempotencyKey == "" {
        http.Error(w, "Header 'Idempotency-Key' is required", http.StatusBadRequest)
        return
    }

    mu.Lock() // Блокируем доступ для других горутин
    if _, exists := processedKeys[idempotencyKey]; exists {
        mu.Unlock() // Не забываем разблокировать перед выходом
        w.WriteHeader(http.StatusOK)
        fmt.Fprintln(w, "Request already processed.")
        return
    }

    // Ключ новый, помечаем его как обрабатываемый
    processedKeys[idempotencyKey] = true
    mu.Unlock() // Разблокируем, чтобы не блокировать другие запросы на время выполнения операции

    // ... здесь логика выполнения платежа ...
    // time.Sleep(2 * time.Second) // Симуляция долгой операции

    w.WriteHeader(http.StatusCreated)
    fmt.Fprintln(w, "Payment processed successfully.")
}