GH GambleHub

Дедуплікація подій

1) Навіщо потрібна дедуплікація

Дублікати з'являються через ретраїв, мережевих таймаутів, відновлень після фейлів і реплея історичних даних. Якщо їх не контролювати:
  • порушуються інваріанти (подвійні списання, повторні email/SMS, «двічі створене» замовлення);
  • зростають витрати (повторні записи/обробки);
  • спотворюється аналітика.

Мета дедуплікації - забезпечити одноразовий спостережуваний ефект при допустимих повторах транспорту, часто разом з ідемпотентністю.

2) Де розміщувати дедуплікацію (рівні)

1. Edge/API-шлюз - відсікаємо явні дублі по'Idempotency-Кеу '/тіло + підпис.
2. Брокер/стрім - логічна дедуплікація по ключу/секвенсу, coalescing при промаху (рідше - через вартість).
3. Приймач подій (consumer) - основне місце: Inbox/таблиця ключів/кеш.
4. Сінк (БД/кеш) - унікальні ключі/UPSERT/версії/компакція.
5. ETL/аналіз - дедуп по вікну часу і ключу в колонкових сторах.

Правило: якомога раніше, але з урахуванням вартості помилкових спрацьовувань і необхідності реплея.

3) Ключі дедуплікації

3. 1 Природні (переважно)

`payment_id`, `order_id`, `saga_id#step`, `aggregate_id#seq`.
Гарантують стабільність і сенс.

3. 2 Складові

`(tenant_id, type, external_id, version)` или `(user_id, event_ts_truncated, payload_hash)`.

3. 3 Відбиток (fingerprint)

Хеш детермінованої підмножини полів (нормалізувати порядок/регістри), опціонально'HMAC (secret, payload)'.

3. 4 Послідовності/версії

Монотонні'seq'per aggregate (оптимістичне блокування/версіонування).

Анти-патерн: «рандомний UUID» без зв'язку з бізнес-сутністю - дедуп неможлива.

4) Тимчасові вікна та порядок

Вікно дедуплікації - період, протягом якого подія може прийти повторно (зазвичай 24-72ч; для фінансів - довше).
Out-of-order: запізнення (lateness). У потокових фреймворках - event time + watermarks.
Sliding/Fix-window дедуп: "чи бачили ключ в останні N хвилин? ».
Sequence-aware: якщо'seq'≤ останнього обробленого - дубль/повтор.

5) Структури даних і реалізації

5. 1 Точний облік (exact)

Redis SET/STRING + TTL: 'SETNX key 1 EX 86400'→ «вперше - обробляємо, інакше - SKIP».
LRU/LFU кеш (in-proc): швидкий, але volatile → краще тільки як перший бар'єр.
SQL унікальні індекси + UPSERT: «встав або онови» (ідемпотентний ефект).

5. 2 Наближені структури (probabilistic)

Bloom/Cuckoo filter: дешева пам'ять, можливі помилкові спрацьовування (false positive). Підходить для явного «галасливого» дропу (наприклад, телеметрія), не для фінансів/замовлень.
Count-Min Sketch: оцінка частот для захисту від «гарячих» дублів.

5. 3 Потокові стани

Kafka Streams/Flink: keyed state store c TTL, дедуп по ключу у вікні; checkpoint/restore.
Watermark + allowed lateness: керує вікном спізнілих подій.

6) Транзакційні патерни

6. 1 Inbox (вхідна таблиця)

Зберігаємо'message _ id '/ключ і результат до побічних ефектів:
pseudo
BEGIN;
ins = INSERT INTO inbox(id, received_at) ON CONFLICT DO NOTHING;
IF ins_not_inserted THEN RETURN cached_result;
result = handle(event);
UPSERT sink with result; -- idempotent sync
UPDATE inbox SET status='done', result_hash=... WHERE id=...;
COMMIT;

Повтор побачить запис і не повторить ефект.

6. 2 Outbox

Бізнес-запис і подія в одній транзакції → публікатор віддає в брокер. Не усуває дубль від споживача, але виключає «дірки».

6. 3 Унікальні індекси/UPSERT

sql
INSERT INTO payments(id, status, amount)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING; -- "create once"
або контрольоване оновлення версії:
sql
UPDATE orders
SET status = $new, version = version + 1
WHERE id=$id AND version = $expected; -- optimistic blocking

6. 4 Версіонування агрегатів

Подія застосовується, якщо'event. version = aggregate. version + 1`. Інакше - дубль/повтор/конфлікт.

7) Дедуп і брокери/стріми

7. 1 Kafka

Idempotent Producer знижує дублі на вході.
Transactions дозволяють атомарно комітити офсети + вихідні записи.
Compaction: зберігає останнє значення per key - пост-фактум дедуп/коалесинг (не для платежів).
Consumer-side: state store/Redis/DB для ключів вікна.

7. 2 NATS / JetStream

Ack/редоставка → at-least-once. Дедуп у споживачі (Inbox/Redis).
JetStream sequence/дюработ споживача спрощують виявлення повторів.

7. 3 Черги (Rabbit/SQS)

Visibility timeout + повторні доставки → потрібен ключ + дедуп-стор.
SQS FIFO з'MessageGroupId '/' DeduplicationId'допомагає, але TTL вікна обмежений провайдером - зберігайте ключі довше, якщо бізнес вимагає.

8) Сховища та аналітиka

8. 1 ClickHouse/BigQuery

Дедуп по вікну: 'ORDER BY key, ts'і'argMax '/' anyLast'з умовою.

ClickHouse:
sql
SELECT key,
anyLast(value) AS v
FROM t
WHERE ts >= now() - INTERVAL 1 DAY
GROUP BY key;

Або матеріалізований шар «унікальних» подій (мердж за ключем/версією).

8. 2 Логи/телеметрія

Припустимо approximate-дедуп (Bloom) на ingest → економимо мережу/диск.

9) Повторна обробка, реплей і бекфілл

Дедуп-ключі повинні переживати реплей (TTL ≥ вікно реплея).
Для бекфілу використовуйте простір ключів з версією ('key #source = batch2025') або окремі «сливи», щоб не заважати онлайновому вікну.
Зберігайте артефакти результату (hash/версію) - це прискорює «fast-skip» на повторах.

10) Метрики і спостережуваність

'dedup _ hit _ total '/' dedup _ hit _ rate'- частка спійманих дублів.
'dedup _ fp _ rate'для ймовірнісних фільтрів.
'window _ size _ seconds'фактичне (по telemetry late arrivals).
`inbox_conflict_total`, `upsert_conflict_total`.
`replayed_events_total`, `skipped_by_inbox_total`.
Профілі по tenant/key/type: де найбільше дублів і чому.

Логи: `message_id`, `idempotency_key`, `seq`, `window_id`, `action=process|skip`.

11) Безпека і приватність

Не кладіть PII в ключ; використовуйте хеші/псевдоніми.
Для підпису відбитка - HMAC (secret, canonical_payload), щоб уникнути колізій/підробки.
Терміни зберігання ключів узгодьте з комплаєнсом (GDPR ретеншн).

12) Продуктивність і вартість

In-proc LRU ≪ Redis ≪ SQL за латентністю/вартістю на операцію.
Redis: дешевий і швидкий, але враховуйте обсяг ключів і TTL; шардуйте по'tenant/hash'.
SQL: дорого по p99, але дає сильні гарантії та аудиторність.
Probabilistic фільтри: дуже дешево, але можливі FP - застосовуйте там, де «зайвий SKIP» не критичний.

13) Анти-патерни

«У нас Kafka exactly-once - ключ не потрібен». Потрібен - в синці/бізнес-шарі.
Занадто короткий TTL для ключів → реплей/затримка доставить дубль.
Глобальний одиночний дедуп-стор → hotspot і SPOF; не шардований по tenant/ключу.
Дедуп тільки в пам'яті - втрата процесу = хвиля дублів.
Bloom для грошей/замовлень - false positive позбавить легітимної операції.
Неузгоджена канонізація payload - різні хеші на ідентичні за змістом повідомлення.
Ігнорування out-of-order - пізні події позначаються дублями помилково.

14) Чек-лист впровадження

  • Визначте природний ключ (або складений/відбиток).
  • Встановіть вікно дедуп і політику'lateness'.
  • Виберіть рівень (я): edge, consumer, sink; передбачте шардування.
  • Реалізуйте Inbox/UPSERT; для потоків - keyed state + TTL.
  • Якщо потрібен approximate-бар'єр - Bloom/Cuckoo (тільки для некритичних доменів).
  • Налаштуйте реплей-сумісність (TTL ≥ вікно реплея/бекфілла).
  • Метрики'dedup _ hit _ rate', конфлікти і лаги вікон; дашборди per-tenant.
  • Game Day: таймаути/ретраї, реплей, out-of-order, падіння кешу.
  • Документуйте канонізацію payload і версіонування ключів.
  • Проведіть навантажувальні тести на «гарячих ключах» і довгих вікнах.

15) Приклади конфігурацій/коду

15. 1 Redis SETNX + TTL (бар'єр)

lua
-- KEYS[1] = "dedup:{tenant}:{key}"
-- ARGV[1] = ttl_seconds local ok = redis. call("SET", KEYS[1], "1", "NX", "EX", ARGV[1])
if ok then return "PROCESS"
else return "SKIP"
end

15. 2 PostgreSQL Inbox

sql
CREATE TABLE inbox (
id text PRIMARY KEY,
received_at timestamptz default now(),
status text default 'received',
result_hash text
);
-- In the handler: INSERT... ON CONFLICT DO NOTHING -> check, then UPSERT in blue.

15. 3 Kafka Streams (дедуп у вікні)

java var deduped = input
.selectKey((k,v) -> v.idempotencyKey())
.groupByKey()
.windowedBy(TimeWindows. ofSizeWithNoGrace(Duration. ofHours(24)))
.reduce((oldV,newV) -> oldV)   // first wins
.toStream()
.map((wKey,val) -> KeyValue. pair(wKey. key(), val));

15. 4 Flink (keyed state + TTL, псевдо)

java
ValueState<Boolean> seen;
env. enableCheckpointing(10000);
onEvent(e):
if (!seen.value()) { process(e); seen. update(true); }

15. 5 NGINX/API-шлюз (Idempotency-Key на edge)

nginx map $http_idempotency_key $idkey { default ""; }
Proxy the key to the backend; backend solves deadup (Inbox/Redis).

16) FAQ

Q: Що вибрати: дедуп чи чиста ідемпотентність?
A: Зазвичай обидва: дедуп - швидкий «фільтр» (економія), ідемпотентність - гарантія правильного ефекту.

Q: Який TTL ставити?
A: ≥ максимального часу можливої повторної доставки + запас. Типово 24-72ч; для фінансів і відкладених завдань - дні/тижні.

Q: Як обробляти пізні події?
A: Налаштуйте'allowed lateness'і сигналізацію'late _ event'; пізні - через окрему гілку (recompute/skip).

Q: Чи можна дедуплікувати весь потік телеметрії?
A: Так, approximate-фільтрами (Bloom) на edge, але враховуйте FP і не застосовуйте до критичних бізнес-ефектів.

Q: Дедуп заважає бекфіллу?
A: Розділяйте простори ключів ('key #batch2025') або відключайте бар'єр на час бекфілу; TTL ключів повинен покривати тільки онлайнові вікна.

17) Підсумки

Дедуплікація - це композиція: правильний ключ, вікно і структура стану + транзакційні патерни (Inbox/Outbox/UPSERT) і усвідомлена робота з порядком і запізнілими подіями. Розмістіть бар'єри там, де це найдешевше, забезпечте ідемпотентність в синках, вимірюйте'dedup _ hit _ rate'і тестуйте реплей/фейли - так ви отримаєте «ефективно exactly-once» без зайвих хвостів латентності і вартості.

Contact

Зв’яжіться з нами

Звертайтеся з будь-яких питань або за підтримкою.Ми завжди готові допомогти!

Розпочати інтеграцію

Email — обов’язковий. Telegram або WhatsApp — за бажанням.

Ваше ім’я необов’язково
Email необов’язково
Тема необов’язково
Повідомлення необов’язково
Telegram необов’язково
@
Якщо ви вкажете Telegram — ми відповімо й там, додатково до Email.
WhatsApp необов’язково
Формат: +код країни та номер (наприклад, +380XXXXXXXXX).

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