Exact o dată vs Cel puțin o dată
1) De ce chiar discuta semantica
Semantica de livrare determină cât de des destinatarul va vedea mesajul atunci când se blochează și se retrage:- Cel mult o dată - fără repetare, dar pierderea este posibilă (rareori acceptabilă).
- Cel puțin o dată - nu pierde, dar duplicate sunt posibile (implicit de cele mai multe brokeri/cozi).
- Exact o dată - fiecare mesaj este procesat exact o dată în ceea ce privește efectul observat.
Adevărul cheie: într-o lume distribuită, fără tranzacții globale și consistență sincronă, un „curat” end-to-end exact-o dată este de neatins. Construim eficient exact-o dată: permitem repetiții pe transport, dar facem procesarea idempotentă, astfel încât efectul observat să fie „ca o dată”.
2) Eșec model și în cazul în care apar duplicate
Reluările apar din cauza:- Pierderi ack/comite (producătorul/brokerul/consumatorul „nu a auzit” confirmarea).
- Realegerea liderilor/replicilor, recuperări după întreruperi de reţea.
- Timeouts/retrageri în orice zone (kliyent→broker→konsyumer→sink).
Consecință: nu vă puteți baza pe „unicitatea livrării” transportului. Gestionați efectele: scrierea în baza de date, debitarea banilor, trimiterea unei scrisori etc.
3) Exact-o dată în furnizori și ceea ce este cu adevărat
3. 1 Kafka
Dă cărămizi:- Producător Idempotent ("activează. idempotence = true ') - previne duplicatele pe partea producătorului atunci când se retrage.
- Tranzacții - publicarea atomică a mesajelor în mai multe loturi și comiterea compensărilor de consum (model de citire-proces-scriere fără „lacune”).
- Compactare - stochează ultima valoare după cheie.
Dar „sfârșitul lanțului” (chiuvetă: DB/plată/e-mail) necesită în continuare idempotență. În caz contrar, dublura handler-ului va provoca un efect dublu.
3. 2 NATS/Iepure/SQS
Implicit este cel puțin o dată cu ack/redelivery. Exact-o dată se realizează la nivelul aplicației: chei, deadstore, upsert.
Concluzie: Transportul exact o dată ≠ efect exact o dată. Acesta din urmă se face în handler.
4) Cum de a construi în mod eficient exact-o dată peste cel puțin o dată
4. 1 Cheie de idempotență
Fiecare comandă/eveniment poartă o cheie naturală: 'payment _ id',' order _ id # step ',' saga _ id # n'. Handler:- Cecuri „deja văzut?” - Dedup-stor (Redis/DB) cu TTL/Retsch.
- Dacă ați văzut, repetă rezultatul calculat anterior sau nu-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 în bază (chiuvetă idempotentă)
Intrările se fac prin UPSERT/ON CONFLICT cu verificarea versiunii/sumei.
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 Outbox tranzactional/Inbox
Outbox: o tranzacție de afaceri și o intrare eveniment-publicare apar în aceeași tranzacție de bază de date. Editorul de fundal citește outbox și trimite brokerului → nu există nici o discrepanță între stat și eveniment.
Inbox: pentru comenzile primite, salvați "message _ id' și rezultatul înainte de execuție; reprocesarea vede înregistrarea și nu repetă efectele secundare.
4. 4 Prelucrarea consistentă a lanțului (read→process→write)
Kafka: tranzacția „a citit offsetul → a notat rezultatele → angajamentului” într-un bloc atomic.
Fără tranzacții: „mai întâi notați rezultatul/Inbox, apoi ack”; cu accident, duplicatul va vedea Inbox și se va termina fără op.
4. 5 SAGA/compensează
Când idempotența este imposibilă (furnizorul extern a notat banii), folosim operațiuni compensatorii (rambursare/anulare) și API-uri externe idempotente (repetarea „POST” cu aceeași „cheie de idempotență” dă același rezultat).
5) Când cel puțin o dată este suficient
Actualizări ale cache-urilor/vizualizări materializate cu compactare bazată pe cheie.
Contoare/valori în cazul în care re-increment este acceptabil (sau stoca delta cu versiunea).
Notificări în cazul în care scrisoarea secundară nu este critică (este mai bine să puneți o cheie oricum).
Regulă: în cazul în care dubla nu schimbă sensul de afaceri sau putem găsi cu ușurință → cel puțin o dată + protecție parțială.
6) Performanță și cost
Exact o dată (chiar și „eficient”) costă mai mult: înregistrările suplimentare (Inbox/Outbox), cheile de stocare, tranzacțiile, diagnosticarea sunt mai dificile.
Cel puțin o dată este mai ieftin/mai simplu, mai bun la debit/p99.
Evaluați: prețul dublei probabilități de × dublă față de costul de protecție.
7) Configurații de probă și cod
7. 1 Producător Kafka (idempotence + tranzacții)
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 Consola Inbox (pseudo cod)
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-uri externe)
POST /payments
Idempotency-Key: 7f1c-42-...
Body: { "payment_id": "p-123", "amount": 10.00 }
POST repetat cu aceeași cheie → același rezultat/stare.
8) Observabilitate și valori
'duplicate _ tentations _ total' - de câte ori a fost prinsă o dublă (conform Inbox/Redis).
„idempotency _ hit _ rate” - proporția repetițiilor „salvate” prin idempotență.
'txn _ abort _ rate' (Kafka/DB) - cota de rollback-uri.
'outbox _ restlog' - publicare lag.
'exactly _ once _ path _ latency {p95, p99}' vs 'at _ least _ once _ path _ latency' - deasupra capului.
Jurnale de audit: o grămadă de 'message _ id',' idempotency _ key ',' saga _ id', 'încercare'.
9) Cărți de joc de testare (Zilele jocului)
Trimiteți reluarea: retraiele producătorului cu temporizări artificiale.
Accident între „chiuvetă și ack”: Asigurați-vă că Inbox/Upsert preveni o dublă.
Re-livrare: creșterea redelivery în broker; verifica dedup.
Idempotența API-urilor externe: POST repetat cu aceeași cheie este același răspuns.
Schimbarea plumbului/întreruperea rețelei: Verificați comportamentul tranzacțiilor/consumatorilor Kafka.
10) Anti-modele
Bazați-vă pe transport: „avem Kafka cu exact-o dată, astfel încât să puteți fără chei” - nu.
No-op ack înainte de înregistrare: ackled, dar chiuveta a scăzut pierderea →.
Lipsa de retrageri DLQ/jitter: reluări interminabile și furtună.
UUID-uri aleatorii în loc de chei naturale: nimic de deduplicat.
Amestecarea Inbox/Outbox cu tabele de producție index-free: încuietori fierbinți și cozi p99.
Tranzacții de afaceri fără API idempotent la furnizori externi.
11) Lista de verificare a selecției
1. Preț dublu (bani/legal/UX) vs preț de protecție (latență/complexitate/cost).
2. Există un eveniment natural/operare cheie? Dacă nu, vino cu unul stabil.
3. Chiuveta suportă Upsert/versioning? În caz contrar - Inbox + compensare.
4. Aveți nevoie de tranzacții globale? Dacă nu, segmentați în SAGA.
5. Aveți nevoie de o reluare/retenție lungă? Kafka + Outbox. Aveți nevoie de RPC rapid/latență scăzută? NATS + Idempotency-Key.
6. Multi-chirie și cote: izolarea cheie/spațiu.
7. Observabilitate: sunt incluse măsurătorile idempotenței și restanțelor.
12) ÎNTREBĂRI FRECVENTE
Î: Este posibil să se realizeze „matematic” exact o dată end-to-end?
R: Numai în scenarii înguste cu un singur magazin consistent și tranzacții până la capăt. În cazul general, nu; utilizați în mod eficient exact o dată prin idempotență.
Î: Care este mai rapid?
R: Cel puțin o dată. Exact-o dată adaugă tranzacții/ → de stocare cheie peste p99 și costul.
Î: În cazul în care pentru a stoca cheile idempotence?
R: Oprire rapidă (Redis) cu TTL sau tabel Inbox (PK = message _ id). Pentru plăți - mai mult (zile/săptămâni).
Î: Cum de a alege tastele TTL dedup?
R: Minim = timp maxim de re-livrare + marjă operațională (de obicei 24-72 ore). Pentru finanțe - mai mult.
Î: Am nevoie de o cheie dacă am compactare prin cheie în Kafka?
R: Da. Compactarea va reduce stocarea, dar nu va face sincronizarea idempotentă.
13) Totaluri
Cel puțin o dată - semantică de transport de bază, fiabilă.
Exact o dată ca efect de afaceri se realizează la nivel de procesor: Idempotency-Key, Inbox/Outbox, Upsert/versiuni, SAGA/compensare.
Alegerea este un compromis al costului ↔ riscului de duplicare ↔ ușurința de funcționare. Proiectați cheile naturale, faceți vânătăile idempotente, adăugați observabilitate și jucați în mod regulat zile de joc - atunci conductele vor fi predictibile și sigure chiar și într-o furtună de retras și eșecuri.