Идемпотетность. Самый бесполезный архитектурный спор

Redis или PostgreSQL для идемпотентности?

Что такое идемпотентность, и зачем она нужна см. в моём предыдущем посте "Двойное списание случается не потому, что кто-то ПЛОХОЙ ИНЖЕНЕР".

Вариант 1. PostgreSQL (Source of Truth)

Базовая схема.

Таблица idempotency_keys в реляционной БД, например, PostgreSQL.

Храним:

  • key — PK, уникальный
  • hash запроса (ловим подмену параметров)
  • тело ответа
  • статус ответа
  • дату создания ключа (для аудита)

Пример таблицы:

CREATE TABLE idempotency_keys ( key text PRIMARY KEY, request_hash text NOT NULL, response_code int NOT NULL, response_body jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now() );

Алгоритм:

  • Начинаешь транзакцию
  • INSERT ... ON CONFLICT DO NOTHING
  • Если вставка прошла → ты первый → выполняешь платёж
  • Если нет → возвращаешь сохранённый ответ

Ключевой момент: проверка + выполнение + сохранение результата — в одной транзакции. Иначе при параллельных запросах словишь двойное списание даже с ключом.

Логика на Go:

// Пытаемся вставить ключ. Если он есть, DO NOTHING вернёт 0 строк. err = tx.QueryRowContext(ctx, ` INSERT INTO idempotency_keys (key, request_hash, response_code, response_body) VALUES ($1, $2, 0, '{}'::jsonb) ON CONFLICT (key) DO NOTHING RETURNING response_code, response_body `, key, reqHash).Scan(&code, &body) if err == nil { // Первый запрос: выполняем бизнес-логику и обновляем запись // ... update idempotency_keys SET response_code = 200 ... return resultBody, 200, tx.Commit() } // Ключ уже был: читаем сохранённый ответ err = tx.QueryRowContext(ctx, ` SELECT response_code, response_body FROM idempotency_keys WHERE key = $1 AND request_hash = $2 -- ВАЖНО: сверяем хеш! `, key, reqHash).Scan(&code, &body)

Redis: где полезен, а где подстава

Redis часто используют так:

  • SET NX key → "забронировали ключ"
  • сделали операцию
  • записали результат

Проблема: если сервис упал между SET NX и сохранением результата — у тебя "processing" навсегда. И клиент будет ходить по кругу.

Поэтому в платежах Redis — это:

  • либо ускоритель
  • либо защита от горячих дублей

Но не источник истины.

Нормальная продовая схема

Комбо:

  • Redis → быстро отсекает повторы в горячем окне (с коротким TTL)
  • PostgreSQL → хранит результат и правду для аудита

Так ты получаешь:

  • скорость
  • надёжность
  • возможность разруливать инциденты

На что смотреть в ревью

Чек-лист:

  • есть ли Idempotency-Key в API (и он обязателен)
  • хранится ли ответ, а не только факт запроса
  • проверяется ли hash запроса
  • всё ли в одной транзакции
  • адекватный TTL (обычно 24ч–7 дней)
  • есть ли cleanup

Вывод

Идемпотентность — это не "архитектурный паттерн ради галочки".
Это штука, которая отделяет:

мы делаем платежи

от

мы иногда списываем деньги дважды, но потом возвращаем

В финтехе второй вариант долго не живёт.

Селькин Андрей
TGM / Go-разработчик из Fintech. Инженерные заметки: https://t.me/andrei_selkin_outbox
2