Exactly-once vs At-least-once
1) Warum überhaupt über Semantik diskutieren?
Die Semantik der Zustellung bestimmt, wie oft der Empfänger die Nachricht bei Ausfällen und Rückfällen sieht:- At-most-once - keine Wiederholungen, aber ein Verlust ist möglich (selten akzeptabel).
- At-least-once - wir verlieren nicht, aber Duplikate sind möglich (Standard der meisten Broker/Warteschlangen).
- Exactly-once - Jede Nachricht wird genau einmal in Bezug auf den beobachteten Effekt verarbeitet.
Die Schlüsselwahrheit: In einer verteilten Welt ohne globale Transaktionen und synchrone Konsistenz ist ein „reines“ Ende-zu-Ende exactly-once unerreichbar. Wir bauen effektiv exactly-once: Wir erlauben Wiederholungen im Transport, aber wir machen die Verarbeitung idempotent, so dass der beobachtete Effekt „wie einmal“ ist.
2) Fehlermodell und wo Duplikate entstehen
Wiederholungen entstehen durch:- ack/commit Verluste (Produzent/Makler/consumer „nicht gehört“ Bestätigung).
- Wiederwahl von Führern/Repliken, Erholung nach Netzlücken.
- Timeouts/Retrails in allen Bereichen (kliyent→broker→konsyumer→sink).
Die Konsequenz: Man könne sich nicht auf die „Einzigartigkeit der Lieferung“ des Transports verlassen. Wir verwalten die Auswirkungen: Schreiben in die Datenbank, Abbuchen von Geld, Senden eines Briefes usw.
3) Exactly-once in Lieferanten und was es wirklich ist
3. 1 Kafka
Gibt Steine:- Idempotent Producer (`enable. idempotence = true') - Verhindert Doppel auf der Produzentenseite bei Retrays.
- Transaktionen - atomare Veröffentlichung von Nachrichten in mehreren Partituren und Kommitat-Offsets des Konsums (Read-Process-Write-Muster ohne „Lücken“).
- Compaction - Speichert den letzten Wert nach Schlüssel.
Aber das „Ende der Kette“ (Synk: DB/Zahlung/Post) erfordert immer noch Idempotenz. Andernfalls bewirkt der doppelte Handler den doppelten Effekt.
3. 2 NATS / Rabbit / SQS
Die Standardeinstellung ist at-least-once mit ack/redelivery. Exactly-once wird auf Anwendungsebene erreicht: Schlüssel, Dedup-Stor, Upsert.
Fazit: Exactly-once Transport ≠ exactly-once Wirkung. Letzteres geschieht im Handler.
4) Wie man effektiv exactly-once auf at-least-once aufbaut
4. 1 Idempotenzschlüssel (idempotency key)
Jedes Team/Ereignis trägt einen natürlichen Schlüssel: 'payment _ id', 'order _ id # step', 'saga _ id # n'. Verarbeiter:- Überprüft „schon gesehen?“ - Dedup-Stor (Redis/DB) mit TTL/Retench.
- Wenn gesehen - wiederholt das zuvor berechnete Ergebnis oder macht einen No-Op.
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 in der Basis (idempotent sync)
Die Einträge erfolgen über UPSERT/ON CONFLICT mit Versions-/Summenprüfung.
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 Transaktionale Outbox/Inbox
Outbox: Die Geschäftstransaktion und der „Event to Publish“ -Eintrag erfolgen in einer einzigen DB-Transaktion. Der Hintergrundpublisher liest die Outbox und sendet sie an den Broker → Es gibt keine Diskrepanzen zwischen dem Status und dem Ereignis.
Inbox: für eingehende Befehle speichern wir 'message _ id' und das Ergebnis bis zur Ausführung; Re-Processing sieht die Aufzeichnung und wiederholt nicht die Nebenwirkungen.
4. 4 Konsistente Kettenverarbeitung (read→process→write)
Kafka: Die Transaktion „las das Offset → schrieb die Ergebnisse des → Commits“ in einen Atomblock.
Keine Transaktionen: „schreiben Sie zuerst das Ergebnis/Inbox, dann ack“; Beim Crash wird das Duplikat Inbox sehen und mit no-op enden.
4. 5 SAGA/Entschädigung
Wenn Idempotenz nicht möglich ist (ein externer Anbieter hat Geld abgebucht), verwenden wir Kompensationsoperationen (refund/void) und idempotente externe APIs (ein wiederholter 'POST' mit demselben 'Idempotency-Key' ergibt das gleiche Ergebnis).
5) Wenn at-least-once genug ist
Aktualisierungen von Caches/materialisierten Ansichten mit Schlüsselkomposition.
Zähler/Metriken, bei denen eine erneute Inkrementierung akzeptabel ist (oder Delta mit Version gespeichert wird).
Benachrichtigungen, bei denen der sekundäre Buchstabe nicht kritisch ist (es ist besser, den Schlüssel trotzdem zu setzen).
Die Regel: Wenn das Take-up die geschäftliche Bedeutung nicht ändert oder leicht zu erkennen ist → at-least-once + teilweiser Schutz.
6) Leistung und Kosten
Exactly-once (auch „effektiv“) kostet mehr: zusätzliche Aufzeichnungen (Inbox/Outbox), Schlüsselspeicherung, Transaktionen, schwierigere Diagnose.
At-least-once ist billiger/einfacher, besser durch throughput/p99.
Bewerten Sie: Der Preis des Doppels × die Wahrscheinlichkeit des Doppels vs die Kosten des Schutzes.
7) Beispiele für Konfigurationen und Code
7. 1 Kafka Produzent (Idempotenz + Transaktionen)
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 mit Inbox (Pseudocode)
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 (externe APIs)
POST /payments
Idempotency-Key: 7f1c-42-...
Body: { "payment_id": "p-123", "amount": 10.00 }
Ein wiederholter POST mit demselben Schlüssel → dasselbe Ergebnis/denselben Status.
8) Beobachtbarkeit und Metriken
'duplicate _ attempts _ total' - wie oft das Double gefangen wurde (per Inbox/Redis).
'idempotency _ hit _ rate' ist der Anteil der durch Idempotenz „geretteten“ Wiederholungen.
'txn _ abort _ rate' (Kafka/DB) - Anteil der Pullbacks.
'outbox _ backlog' - Veröffentlichungsrückstand.
'exactly _ once _ path _ latency {p95, p99}' vs' at _ least _ once _ path _ latency 'ist der Overhead.
Prüfung der Protokolle: Bündel 'message _ id', 'idempotency _ key', 'saga _ id', 'attempt'.
9) Test-Playbooks (Spieltage)
Sendungswiederholung: Produzenten-Retrays unter künstlichen Timeouts.
Crash zwischen „Synk und Ack“: Stellen Sie sicher, dass Inbox/Upsert ein Double verhindern.
Re-Lieferung: Erhöhen Sie die Redelivery im Broker; Überprüfen Sie den Dedup.
Externe API-Idempotenz: Wiederholte POSTs mit demselben Schlüssel sind die gleiche Antwort.
Führungswechsel/Netzwerkbruch: Überprüfen Sie Kafka-Transaktionen/Consumer-Verhalten.
10) Anti-Muster
Auf den Transport zu setzen: „Wir haben Kafka mit exactly-once, also können Sie ohne Schlüssel“ - nein.
No-op ack vor der Aufnahme: ackerte, aber der Sync fiel → Verlust.
Keine DLQ/Retrails mit Jitter: endlose Wiederholungen und Sturm.
Zufällige UUIDs statt natürlicher Schlüssel: nichts zu deduplizieren.
Inbox/Outbox mit Prod-Tabellen ohne Indizes mischen: Hot Locks und p99 Tails.
Geschäftsbetrieb ohne idempotente API bei externen Anbietern.
11) Checkliste der Wahl
1. Doppelpreis (Geld/Recht/UX) vs Schutzpreis (Latenz/Komplexität/Kosten).
2. Gibt es einen natürlichen Schlüssel für ein Ereignis/eine Operation? Wenn nicht, kommen Sie mit einem stabilen.
3. Unterstützt Sink Upsert/Versionierung? Ansonsten - Inbox + Entschädigung.
4. Sind globale Transaktionen erforderlich? Wenn nicht, segmentieren Sie in SAGA.
5. Replay/lange Retention erforderlich? Kafka + Outbox. Benötigen Sie schnelle RPCs/niedrige Latenz? NATS + Idempotency-Key.
6. Multi-Tenant und Quoten: Isolierung von Schlüsseln/Räumen.
7. Beobachtbarkeit: Die Metriken idempotency und backlog sind enthalten.
12) FAQ
F: Kann man „mathematisch“ exactly-once end-to-end erreichen?
A: Nur in engen Szenarien mit einem konsistenten Speicher und Transaktionen auf dem ganzen Weg. Im Allgemeinen - nein; Verwenden Sie effektiv exactly-once durch idempotence.
F: Was ist schneller?
A: At-least-once. Exactly-once fügt Transaktionen/Schlüssel-Speicherung → über p99 und Kosten.
F: Wo kann ich Idempotence-Schlüssel aufbewahren?
A: Schnellstor (Redis) mit TTL oder Inbox-Tabelle (PK = message _ id). Für Zahlungen - länger (Tage/Wochen).
F: Wie wählt man TTL-Dedup-Schlüssel?
A: Minimum = maximale Nachlieferungszeit + Betriebsbestand (normalerweise 24-72 Stunden). Für die Finanzen mehr.
F: Brauche ich einen Schlüssel, wenn ich compaction by key in Kafka habe?
A: Ja. Compaction wird die Lagerung reduzieren, aber Ihren Sync nicht idempotent machen.
13) Ergebnisse
At-least-once ist eine grundlegende, zuverlässige Transportsemantik.
Exactly-once als Geschäftseffekt wird auf Handler-Ebene erreicht: Idempotency-Key, Inbox/Outbox, Upsert/Version, SAGA/Compensation.
Die Wahl ist ein Kompromiss aus Kosten ↔ Risiko ↔ einfacher Bedienung. Entwerfen Sie natürliche Schlüssel, machen Sie Syncs idempotent, fügen Sie Beobachtbarkeit hinzu und verbringen Sie regelmäßig Spieltage - dann sind Ihre Piplines vorhersehbar und sicher, selbst bei einem Sturm von Retrays und Ausfällen.