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).

Натискаючи кнопку, ви погоджуєтесь на обробку даних.