GH GambleHub

Exactly-once vs At-least-once

1) Pourquoi parler de sémantique du tout

La sémantique de livraison détermine la fréquence à laquelle le destinataire voit le message en cas d'échec et de rétractation :
  • At-most-once - pas de répétitions, mais une perte possible (rarement acceptable).
  • At-least-once - nous ne perdons pas, mais des doublons sont possibles (défaut de la plupart des courtiers/files d'attente).
  • Exactly-once - chaque message est traité exactement une fois en termes d'effet observé.

Une vérité clé : dans un monde distribué, sans transactions globales et sans cohérence synchrone, le « pur » end-to-end exactly-once est impossible à atteindre. Nous construisons effectivement exactly-once : nous autorisons les répétitions dans le transport, mais nous rendons le traitement idempotent de sorte que l'effet observé soit « comme une seule fois ».


2) Modèle de défaillance et où les doublons se produisent

Les répétitions apparaissent à cause de :
  • Pertes ack/commit (producteur/courtier/consumer « n'a pas entendu » la confirmation).
  • Réélections de leaders/répliques, restaurations après des ruptures de réseau.
  • Timeuts/retraits sur toutes les sections (kliyent→broker→konsyumer→sink).

La conséquence : on ne peut pas compter sur "l'originalité de la livraison" le transport. Nous gérons les effets : écriture dans la base de données, prélèvement d'argent, envoi d'une lettre, etc.


3) Exactly-once dans les fournisseurs et ce qu'il est vraiment

3. 1 Kafka

Donne des briques :
  • Idempotent Producer (`enable. idempotence = true ') - empêche les prises du côté du producteur lors des retraits.
  • Transactions - publient atomiquement des messages en plusieurs lots et commettent des offsets de consommation (modèle read-process-write sans « pass »).
  • Compaction : Stocke la dernière valeur par clé.

Mais la « fin de la chaîne » (sink : OBD/paiement/courrier) exige toujours l'idempotence. Sinon, la prise du processeur déclenchera la prise de l'effet.

3. 2 NATS / Rabbit / SQS

La valeur par défaut est at-least-once avec ack/redelivery. Exactly-once est atteint au niveau de l'application : clés, dedup stor, upsert.

Conclusion : Exactly-once par transport ≠ exactly-once par effet. Ce dernier se fait dans le processeur.


4) Comment construire efficacement exactly-once sur at-least-once

4. 1 Clé idempotente (clé idempotente)

Chaque commande/événement porte une clé naturelle : 'payment _ id', 'order _ id # step', 'saga _ id # n'. Processeur :
  • Vérifie « déjà vu ? » - dedup store (Redis/OBD) avec TTL/rétention.
  • Si vu - répète le résultat précédemment calculé ou fait no-op.
Redis-croquis :
lua
-- SET key if not exists; expires in 24h local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", 86400)
if ok then return "PROCESS" else return "SKIP" end

4. 2 Upsert dans la base (sinc idempotent)

Les enregistrements sont effectués par UPSERT/ON CONFLICT avec vérification de version/montant.

PostgreSQL:
sql
INSERT INTO payments(id, status, amount, updated_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (id) DO UPDATE
SET status = EXCLUDED.status,
updated_at = now()
WHERE payments.status <> EXCLUDED.status;

4. 3 Transactionnel Outbox/Inbox

Outbox : la transaction d'entreprise et l'enregistrement « événement de publication » se produisent dans une seule transaction OBD. L'éditeur d'arrière-plan lit l'outbox et envoie au courtier → aucune différence entre l'état et l'événement.
Inbox : pour les commandes entrantes, nous enregistrons 'message _ id' et le résultat avant l'exécution ; le traitement répété voit l'enregistrement et ne répète pas les effets secondaires.

4. 4 Traitement de chaîne de consistance (read→process→write)

Kafka : transaction « lu offset → enregistré les résultats → commit » dans un bloc atomique.
Pas de transactions : « d'abord écrire le résultat/Inbox, puis ack » ; au crash, le doublon verra Inbox et se terminera en no-op.

4. 5 SAGA/indemnisation

Lorsque l'idempotence n'est pas possible (le fournisseur externe a débité l'argent), on utilise les opérations de compensation (refund/void) et les API externes idempotents (la répétition « POST » avec le même « Idempotency-Key » donne le même total).


5) Quand at-least-once suffit

Mises à jour des caches/vues matérialisées avec compaction par clé.
Compteurs/métriques où la réincrémentation est acceptable (ou stocker des deltas avec la version).
Notifications où l'écriture secondaire n'est pas critique (mieux vaut mettre la clé de toute façon).

Règle : si la prise ne change pas le sens de l'entreprise ou si nous détectons facilement la → at-least-once + protection partielle.


6) Performance et coût

Exactly-once (même « efficace ») coûte plus cher : doper les enregistrements (Inbox/Outbox), stocker les clés, les transactions, le diagnostic est plus difficile.
At-least-once est moins cher/plus facile, mieux par throughput/p99.
Évaluez : le prix de la prise × la probabilité de la prise vs le coût de la protection.


7) Exemples de configurations et de code

7. 1 Kafka producteur (idempotence + transactions)

properties enable.idempotence=true acks=all retries=INT_MAX max.in.flight.requests.per.connection=5 transactional.id=orders-writer-1
java producer.initTransactions();
producer.beginTransaction();
producer.send(recordA);
producer.send(recordB);
// также можно atomically commit consumer offsets producer.commitTransaction();

7. 2 Consumer avec Inbox (pseudo-code)

pseudo if (inbox.exists(msg.id)) return inbox.result(msg.id)
begin tx if!inbox.insert(msg.id) then return inbox.result(msg.id)
result = handle(msg)
sink.upsert(result)     # идемпотентный синк inbox.set_result(msg.id, result)
commit ack(msg)

7. 3 HTTP Idempotency-Key (API externes)


POST /payments
Idempotency-Key: 7f1c-42-...
Body: { "payment_id": "p-123", "amount": 10.00 }

Un POST répété avec la même clé → le même résultat/statut.


8) Observabilité et métriques

'duplicate _ attempts _ total' - combien de fois la prise a été prise (par Inbox/Redis).
'idempotency _ hit _ rate' est la proportion de répétitions « sauvées » par l'idempotentialité.
'txn _ abort _ rate' (Kafka/OBD) est la proportion de rebonds.
'outbox _ backlog' est un retard de publication.
'exactly _ once _ path _ latency {p95, p99}' vs'at _ least _ once _ path _ latency 'est un frais généraux.
Audit des logs : lien 'message _ id', 'idempotency _ key', 'saga _ id', 'attempt'.


9) Test-playbooks (Game Days)

Répétition de l'envoi : retraits du producteur sur les timaouts artificiels.
Crash entre « sink et ack » : assurez-vous que Inbox/Upsert empêche la prise.
Pere-livraison : augmenter la redelivery dans le courtier ; Vérifiez le dedup.
Idempotence des API externes : les POST répétés avec la même clé sont la même réponse.
Changement de leader/rupture de réseau : vérifier les transactions Kafka/comportement des consumers.


10) Anti-modèles

Compter sur le transport : « Nous avons Kafka avec exactly-once, donc vous pouvez sans clés » - non.
No-op ack avant l'enregistrement : ackned, mais le bleu est tombé → perte.
Absence de DLQ/rétracteurs avec jitter : répétitions sans fin et tempête.
UUID aléatoires au lieu de clés naturelles : rien à dédupliquer.
Mélange Inbox/Outbox avec tables pro sans index : verrouillages chauds et p99-queues.
Opérations commerciales sans API idempotent chez des fournisseurs externes.


11) Chèque de sélection

1. Prix du double (argent/juridique/UX) vs prix de la protection (latence/complexité/coût).
2. Y a-t-il une clé naturelle d'événement/d'opération ? Sinon, trouvez un stable.
3. Sink supporte-t-il Upsert/versioning ? Sinon, Inbox + compensation.
4. Les transactions globales sont-elles nécessaires ? Sinon, segmentez-le en SAGA.
5. Nécessite-t-il un relais/un long rappel ? Kafka + Outbox. Besoin d'un RPC rapide/faible latence ? NATS + Idempotency-Key.
6. Multi-tenance et quotas : isolation des clés/espaces.
7. Observabilité : les métriques idempotency et backlog sont incluses.


12) FAQ

Q : Est-il possible d'atteindre « mathématique » exactly-once end-to-end ?
R : Uniquement dans les scénarios étroits avec un seul stockage cohérent et des transactions sur tout le chemin. En général, non ; utiliser efficacement exactly-once par idempotence.

Q : Quoi de plus rapide ?
A: At-least-once. Exactly-once ajoute les transactions/stockage de clés → au-dessus de p99 et le coût.

Q : Où stocker les clés d'idempotence ?
A : Store rapide (Redis) avec TTL, ou table Inbox (PK = message _ id). Pour les paiements - plus longtemps (jours/semaines).

Q : Comment choisir les clés de déduplication TTL ?
A : Minimum = délai de livraison maximal + stock opérationnel (habituellement 24-72 h). Pour la finance - plus.

Q : Ai-je besoin d'une clé si j'ai une compaction par clé dans Kafka ?
A : Oui. Compaction réduira le stockage, mais ne rendra pas votre sink idempotent.


13) Résultats

At-least-once est une sémantique de base et fiable du transport.
Exactly-once en tant qu'effet commercial est atteint au niveau du processeur : Idempotency-Key, Inbox/Outbox, Upsert/version, SAGA/compensation.
Le choix est un compromis entre le coût ↔ le risque de prise ↔ la facilité d'utilisation. Concevez des clés naturelles, faites des singes idempotentes, ajoutez de l'observabilité et passez régulièrement des journées de jeu - vos piplines seront alors prévisibles et sécurisées, même en cas de tempête de rétrécissements et d'échecs.

Contact

Prendre contact

Contactez-nous pour toute question ou demande d’assistance.Nous sommes toujours prêts à vous aider !

Commencer l’intégration

L’Email est obligatoire. Telegram ou WhatsApp — optionnels.

Votre nom optionnel
Email optionnel
Objet optionnel
Message optionnel
Telegram optionnel
@
Si vous indiquez Telegram — nous vous répondrons aussi là-bas.
WhatsApp optionnel
Format : +code pays et numéro (ex. +33XXXXXXXXX).

En cliquant sur ce bouton, vous acceptez le traitement de vos données.