Ответ
Идемпотентность — это свойство операции, при котором повторный вызов с теми же входными данными даёт тот же результат, что и первичный. Это критически важно в бэкенд-системах, чтобы, например, не списать деньги с клиента дважды из-за сетевого сбоя и повторного запроса (retry).
Классический подход — использование ключа идемпотентности (Idempotency-Key).
Алгоритм выглядит так:
- Клиент генерирует уникальный ключ (например,
UUID) для каждой операции и передает его в заголовке запроса (Idempotency-Key). - Сервер перед выполнением операции проверяет этот ключ в хранилище (например, Redis или БД).
- Возможные сценарии:
- Ключа нет: Это новый запрос. Сервер начинает транзакцию, выполняет бизнес-логику, сохраняет результат и ключ идемпотентности в БД атомарно. После этого возвращает результат клиенту.
- Ключ есть: Это повторный запрос. Сервер не выполняет бизнес-логику заново, а сразу возвращает сохраненный ранее результат.
Пример реализации с Redis и PostgreSQL:
// PaymentProcessor обрабатывает платежи
type PaymentProcessor struct {
cache *redis.Client // Для быстрой проверки ключа
db *sql.DB // Для атомарного выполнения операции
}
func (p *PaymentProcessor) HandlePayment(ctx context.Context, req *PaymentRequest) (*PaymentResponse, error) {
idempotencyKey := req.Header.Get("Idempotency-Key")
if idempotencyKey == "" {
return nil, errors.New("idempotency key is missing")
}
// 1. Быстрая проверка в кеше (Redis)
if status := p.cache.Get(ctx, idempotencyKey).Val(); status == "completed" {
// Можно вернуть сохраненный ответ или просто подтверждение
return &PaymentResponse{Status: "OK"}, nil
}
// 2. Атомарная операция в БД
tx, err := p.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback() // Откатится, если не будет Commit
// Проверяем ключ в таблице идемпотентности внутри транзакции, чтобы избежать race condition
var count int
tx.QueryRowContext(ctx, "SELECT count(*) FROM idempotency_keys WHERE key = $1", idempotencyKey).Scan(&count)
if count > 0 {
return &PaymentResponse{Status: "OK"}, nil // Другой запрос уже обработал
}
// 3. Выполнение бизнес-логики
if err := processPaymentInDB(tx, req); err != nil {
return nil, err
}
// 4. Сохраняем ключ идемпотентности
_, err = tx.ExecContext(ctx, "INSERT INTO idempotency_keys (key) VALUES ($1)", idempotencyKey)
if err != nil {
return nil, err
}
// 5. Фиксируем транзакцию и кешируем результат
if err := tx.Commit(); err != nil {
return nil, err
}
p.cache.Set(ctx, idempotencyKey, "completed", 24*time.Hour) // TTL для очистки старых ключей
return &PaymentResponse{Status: "OK"}, nil
}
Ключевые моменты этого подхода:
- Redis используется как быстрый фильтр для уже обработанных запросов.
- Транзакция в PostgreSQL гарантирует атомарность: либо операция и сохранение ключа выполняются вместе, либо ничего не происходит.
- TTL (Time To Live) в Redis необходим, чтобы не хранить ключи вечно.
Ответ 18+ 🔞
Да ты посмотри, какая хуйня, блядь! Идемпотентность, ёпта! Это ж когда ты десять раз нажимаешь на кнопку, а у тебя только один раз деньги списывают, а не десять, понимаешь? А то бывает — клик-клик, а потом смотришь, а с карты всё, пиздец, вынесли, как будто ты не покупку делал, а на войну снаряды закупал.
Вот чтобы эта мартышлюшка не происходила, умные дядьки придумали ключ идемпотентности. Суть проще пареной репы, блядь:
- Перед тем как просить сервер что-то сделать, клиент выдумывает уникальную хуйню —
Idempotency-Key. Типа паспорт для запроса. - Сервер этот паспорт проверяет: "А этот мудак уже тут был?"
- Если не был — всё делает, результат запоминает и паспорт в альбомчик вклеивает.
- Если был — даже не парится, а сразу из альбомчика достаёт старый ответ и отдаёт. И всё, блядь!
А теперь смотри, как это в коде выглядит, только не засыпай, а то хуй с горы на тебя упадёт.
// PaymentProcessor обрабатывает платежи
type PaymentProcessor struct {
cache *redis.Client // Для быстрой проверки ключа
db *sql.DB // Для атомарного выполнения операции
}
func (p *PaymentProcessor) HandlePayment(ctx context.Context, req *PaymentRequest) (*PaymentResponse, error) {
idempotencyKey := req.Header.Get("Idempotency-Key")
if idempotencyKey == "" {
return nil, errors.New("idempotency key is missing")
}
// 1. Быстрая проверка в кеше (Redis)
if status := p.cache.Get(ctx, idempotencyKey).Val(); status == "completed" {
// Можно вернуть сохраненный ответ или просто подтверждение
return &PaymentResponse{Status: "OK"}, nil
}
// 2. Атомарная операция в БД
tx, err := p.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback() // Откатится, если не будет Commit
// Проверяем ключ в таблице идемпотентности внутри транзакции, чтобы избежать race condition
var count int
tx.QueryRowContext(ctx, "SELECT count(*) FROM idempotency_keys WHERE key = $1", idempotencyKey).Scan(&count)
if count > 0 {
return &PaymentResponse{Status: "OK"}, nil // Другой запрос уже обработал
}
// 3. Выполнение бизнес-логики
if err := processPaymentInDB(tx, req); err != nil {
return nil, err
}
// 4. Сохраняем ключ идемпотентности
_, err = tx.ExecContext(ctx, "INSERT INTO idempotency_keys (key) VALUES ($1)", idempotencyKey)
if err != nil {
return nil, err
}
// 5. Фиксируем транзакцию и кешируем результат
if err := tx.Commit(); err != nil {
return nil, err
}
p.cache.Set(ctx, idempotencyKey, "completed", 24*time.Hour) // TTL для очистки старых ключей
return &PaymentResponse{Status: "OK"}, nil
}
И что тут у нас, блядь? А вот что, в рот меня чих-пых!
- Редиска (Redis) — это наш швейцар, быстрый такой. Он сразу видит, если этот паспорт уже в базе был, и говорит: "Иди нахуй, всё уже сделано, не мешай работать".
- Транзакция в Постгресе — это чтобы не было вот этой ебалы, когда два запроса одновременно прилетели и оба думают, что они первые. Транзакция всё скрепляет, как бульдожья хватка: либо операция и сохранение ключа прошли вместе, либо нихуя не прошло. Никаких промежуточных состояний, блядь!
- TTL — это срок годности паспорта. А то накопится этих ключей овердохуища, и швейцар с ума сойдёт. Через сутки — нахуй, в мусорку.
Вот и вся магия, ёпта. Сделал так — и спи спокойно, не бойся, что из-за кривой сети или нервного пользователя с карты сотку снимут десять раз.