GH GambleHub

Идемпотентность и ключи

Что такое идемпотентность

Идемпотентность — свойство операции, при котором повтор с тем же идентификатором не меняет итогового эффекта. В распределенных системах это основной способ сделать результат эквивалентным «ровно одной обработке», несмотря на ретраи, дубликаты сообщений и таймауты.

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

Где это важно

Платежи и балансы: списания/зачисления по `operation_id`.
Бронирования/квоты/лимиты: один и тот же слот/ресурс.
Вебхуки/уведомления: повторная доставка не должна дублировать эффект.
Импорт/миграции: повторный прогон файлов/пакетов.
Стрим-процессинг: дубли из брокера/CDC.

Виды ключей и область их действия

1. Operation key — идентификатор конкретной попытки бизнес-операции

Примеры: `idempotency_key` (HTTP), `operation_id` (RPC).
Область: сервис/агрегат; хранится в таблице дедупликации.

2. Event key — уникальный идентификатор события/сообщения

Примеры: `event_id` (UUID), `(producer_id, sequence)`.
Область: потребитель/группа потребителей; защищает проекции.

3. Business key — естественный ключ предметной области

Примеры: `payment_id`, `invoice_number`, `(user_id, day)`.
Область: агрегат; применяется в проверках уникальности/версии.

💡 Часто используются вместе: `operation_id` защищает команду, `event_id` — доставку, `business key` — инварианты агрегата.

TTL и политика хранения

TTL ключей ≥ возможного окна повторов: ретенция лога + сетевые/процессные задержки.
Для критичных доменов (платежи) TTL — дни/недели; для телеметрии — часы.
Чистите дедуп-таблицы бэкграунд-джобами; для аудита — архивируйте.

Хранилища для ключей (дедупликация)

Транзакционная БД (рекомендуется): надежные upsert/unique-индексы, совместная транзакция с эффектом.
KV/Redis: быстро, удобно для короткого TTL, но без совместной транзакции с OLTP — осторожно.
State store стрим-процессора: локально + чейнджлог в брокере; хорошо в Flink/KStreams.

Схема (вариант в БД):
  • idempotency_keys

`consumer_id` (или `service`), `op_id` (PK на пару), `applied_at`, `ttl_expires_at`, `result_hash`/`response_status` (опц.).

Индексы: `(consumer_id, op_id)` — уникальный.

Базовые приемы реализации

1) Транзакция «эффект + прогресс»

Запись результата и фиксация прогресса чтения/позиции — в одной транзакции.

pseudo begin tx if not exists(select 1 from idempotency_keys where consumer=:c and op_id=:id) then
-- apply effect atomically (upsert/merge/increment)
apply_effect(...)
insert into idempotency_keys(consumer, op_id, applied_at)
values(:c,:id, now)
end if
-- record reading progress (offset/position)
upsert offsets set pos=:pos where consumer=:c commit

2) Optimistic Concurrency (версия агрегата)

Защищает от двойного эффекта при гонках:
sql update account set balance = balance +:delta,
version = version + 1 where id=:account_id and version=:expected_version;
-- if 0 rows are updated → retry/conflict

3) Идемпотентные sinks (upsert/merge)

Операция «начислить один раз»:
sql insert into bonuses(user_id, op_id, amount)
values(:u,:op,:amt)
on conflict (user_id, op_id) do nothing;

Идемпотентность в протоколах

HTTP/REST

Заголовок `Idempotency-Key: <uuid|hash>`.
Сервер хранит запись ключа и повторно возвращает тот же ответ (или код `409`/`422` при конфликте инвариантов).
Для «небезопасных» POST — обязателен `Idempotency-Key` + устойчивый таймаут/ретрай-политика.

gRPC/RPC

Метаданные `idempotency_key`, `request_id` + deadline.
Серверная реализация — как в REST: таблица дедупа в транзакции.

Брокеры/стриминг (Kafka/NATS/Pulsar)

Продюсер: стабильный `event_id`/идемпотентный продюсер (где поддерживается).
Консьюмер: дедуп по `(consumer_id, event_id)` и/или по бизнес-версии агрегата.
Отдельный DLQ для неидемпотентных/поврежденных сообщений.

Вебхуки и внешние партнеры

Требуйте `Idempotency-Key`/`event_id` в контракте; повторная доставка должна быть безопасной.
Храните `notification_id` и статусы отправки; при ретрае — не дублируйте.

Проектирование ключей

Детерминированность: ретраи должны присылать тот же ключ (генерируйте заранее на клиенте/оркестраторе).
Область видимости: формируйте `op_id` как `service:aggregate:id:purpose`.
Коллизии: используйте UUIDv7/ULID или хэш от бизнес-параметров (с солью при необходимости).
Иерархия: общий `operation_id` на фронте → транслируется во все подоперации (идемпотентная цепочка).

UX и продуктовые аспекты

Повторный запрос по ключу должен возвращать тот же результат (включая тело/статус), либо явное «уже выполнено».
Отображайте пользователю статусы «операция обрабатывается/завершена» вместо повторной попытки «на удачу».
Для долгих операций — поллинг по ключу (`GET /operations/{op_id}`).

Наблюдаемость

Логируйте `op_id`, `event_id`, `trace_id`, исход: `APPLIED` / `ALREADY_APPLIED`.
Метрики: доля повторов, размер дедуп-таблиц, время транзакций, конфликтов версий, DLQ-ставка.
Трейс: ключ должен проходить через команду → событие → проекцию → внешний вызов.

Безопасность и комплаенс

Не храните PII в ключах; ключ — идентификатор, не payload.
Шифруйте чувствительные поля в записях дедупа при длительном TTL.
Политика хранения: TTL и архивы; право на забвение — через крипто-стирание ответов/метаданных (если они содержат PII).

Тестирование

1. Дубликаты: прогон одного сообщения/запроса 2–5 раз — эффект ровно один.
2. Падение между шагами: до/после записи эффекта, до/после фиксации офсета.
3. Рестарт/ребаланс потребителей: нет двойного применения.
4. Конкуренция: параллельные запросы с одним `op_id` → один эффект, второй — `ALREADY_APPLIED/409`.
5. Долгоживущие ключи: проверка истечения TTL и повторов после восстановления.

Антипаттерны

Случайный новый ключ на каждый ретрай: система не распознает повторы.
Два отдельные коммита: сначала эффект, потом офсет — падение между ними дублирует эффект.
Доверие только брокеру: отсутствие дедупа в синке/агрегате.
Отсутствие версии агрегата: повторное событие меняет состояние второй раз.
Fat keys: ключ включает бизнес-поля/PII → утечки и сложные индексы.
Отсутствие повторяемых ответов: клиент не может безопасно ретраить.

Примеры

Платежный POST

Клиент: `POST /payments` + `Idempotency-Key: k-789`.
Сервер: транзакция — создает `payment` и запись в `idempotency_keys`.
Повтор: возвращает тот же `201`/тело; при конфликте инварианта — `409`.

Начисление бонуса (sink)

sql insert into credits(user_id, op_id, amount, created_at)
values(:u,:op,:amt, now)
on conflict (user_id, op_id) do nothing;

Проекция из событий

Консьюмер хранит `seen(event_id)` и `version` агрегата; повтор — игнор/идемпотентный upsert.
Прогресс чтения фиксируется в той же транзакции, что и обновление проекции.

Чек-лист продакшена

  • Для всех небезопасных операций определен идемпотентный ключ и его область видимости.
  • Есть таблицы дедупа с TTL и уникальными индексами.
  • Эффект и прогресс чтения коммитятся атомарно.
  • В write-модели включена оптимистичная конкуренция (версия/sequence).
  • Контракты API фиксируют `Idempotency-Key`/`operation_id` и поведение повторов.
  • Метрики и логи содержат `op_id`/`event_id`/`trace_id`.
  • Тесты на дубликаты, падения и гонки — в CI.
  • Политика TTL/архива и безопасность PII соблюдены.

FAQ

Чем `Idempotency-Key` отличается от `Request-Id`?
`Request-Id` — трассировка; он может меняться на ретраях. `Idempotency-Key` — семантический идентификатор операции, обязателен одинаковый при повторах.

Можно ли делать идемпотентность без БД?
Для короткого окна — да (Redis/внутрипроцессный кеш), но без совместной транзакции растет риск дублей. В критичных доменах — лучше в одной БД-транзакции.

Что делать с внешними партнерами?
Договаривайтесь о ключах и повторяемых ответах. Если партнер не поддерживает — оборачивайте вызов в свой идемпотентный слой и храните «уже применялось».

Как выбирать TTL?
Суммируйте максимальные задержки: ретенция лога + worst-case сети/ребаланса + буфер. Добавляйте запас (×2).

Итог

Идемпотентность — это дисциплина ключей, транзакций и версий. Устойчивые идентификаторы операций + атомарная фиксация эффекта и прогресса чтения + идемпотентные sinks/проекции дают «ровно один эффект» без магии транспортного уровня. Делайте ключи детерминированными, TTL — реалистичными, а тесты — злонамеренными. Тогда ретраи и дубликаты станут рутиной, а не инцидентами.

Contact

Свяжитесь с нами

Обращайтесь по любым вопросам или за поддержкой.Мы всегда готовы помочь!

Начать интеграцию

Email — обязателен. Telegram или WhatsApp — по желанию.

Ваше имя необязательно
Email необязательно
Тема необязательно
Сообщение необязательно
Telegram необязательно
@
Если укажете Telegram — мы ответим и там, в дополнение к Email.
WhatsApp необязательно
Формат: +код страны и номер (например, +380XXXXXXXXX).

Нажимая кнопку, вы соглашаетесь на обработку данных.