Strategii de repetiție și idempotență
1) De ce aveți nevoie de ea
În rețele, defecțiunile sunt norma: timeout, erori tranzitorii, flapping-uri de rețea, supraîncărcare. Retragerile îmbunătățesc fiabilitatea numai dacă:1. se repetă în condiții de siguranță (idempotent)
2. se observă întârzieri între repetări,
3. limitele/cotele și dependențele „sănătate” sunt respectate.
Scopul este comportamentul efectiv-o dată la nivelul operațiunilor de afaceri fără ia false și curse.
2) Taxonomia semanticii de livrare
At-most-once: nu repetare, risc de pierdere (logare, foc-și-uita).
Cel puțin o dată: sunt posibile duplicate → este necesară idempotența consumatorilor (cele mai multe cozi, cărți web).
Efectiv-o dată: duplicate sunt posibile, dar deduplicat corect (chei, tranzacții, outbox).
3) Când să se retragă și când nu
Retragerea are sens: '408', '429' (observând 'Retry-After'), '425' (Prea devreme), '499' (client închis pe perimetru), '5xx', '504', pauze de reţea, '502' la poarta de acces, 'connection reset'.
Nu retrage fără a schimba interogarea: '400/ 401/403/404/422'.
Cazuri controversate: „409 Conflict” (nu de obicei retraim; mai întâi citim starea operațiunii/reconfirmăm intenția).
4) Timeouts, backoff și jitter
4. 1 Reguli
În primul rând timeout, apoi retro: fiecare cerere trebuie să aibă un „termen limită”.
Backoff exponențial: 'delay _ n = base 2 ^ n', limit' max _ delay '.
Jitter este necesar: adăugați aleatoriu pentru a decupla „unde sincrone plictisitoare”.
4. 2 modele Jitter
Full jitter: 'sleep = rand (0, base2 ^ n)' este cea mai bună alegere generală.
Jitter decorat: 'somn = min (max_delay, rand (bază, sleep_prev3))' - pentru dialoguri lungi.
Equal jitter: 'sleep = base2 ^ n/2 + rand (0, base2 ^ n/2)' - variație moale.
4. 3 Reîncercare-buget
Limitarea proporției de retribuții:- 'retry _ budget _ per _ min = max (α success_rps, floor β)'; de obicei 'α = 0. 1–0. 2`.
- Dacă bugetul este epuizat, treceți la întrerupătorul „deschis”.
5) Interacțiunea cu limitarea ratei și întrerupătorul de circuit
Respectați „Retry-After”, „RateLimit-Reset” și numărați-l în back-off.
La high '5xx '/timeout-uri - coborâți frecvența retray și concurența generală.
- Jumătate deschis: Permite eșantionarea limitată.
- Deschis: respinge instantaneu (salvează resursă).
- Închis: muncă obișnuită.
- La operațiile de scriere, este de preferat să se întoarcă 409/503 cu un indiciu clar decât răsuciți retraiele agresive.
6) Idempotența operațiunilor de scriere
6. 1 Idee generală
Aceleași intenții → un singur rezultat. Baza este cheia de idempotență și stocarea înregistrărilor de execuție.
6. 2 Contract HTTP
Clientul trimite antetul:
Idempotency-Key: 7a6b7f9e-2a46-4d0b-9c3a-2b30e1c3c9e3
Idempotency-Key-Expiry: 24h # optional
Server:
- Salvează (cheie, rezultat → stare, hash corp) la primul succes
- dacă se repetă, returnează răspunsul vechi și antetul 'Idempotency-Replay: true';
- în cazul unui conflict corporal (aceeași cheie, dar o sarcină utilă diferită) - „409 Conflict”.
6. 3 Stocare și TTL
Tabel/valoare cheie: 'idempotency _ key', 'request _ hash', 'result', 'status', 'expiration _ at'.
TTL = fereastră de reluări posibile și livrări întârziate (de obicei 24-72 ore pentru plăți).
Indicii prin 'idempotency _ key'; pentru sarcină mare - hash sharding.
6. 4 Exemplu Schema (SQL)
sql
CREATE TABLE idempo_store (
key UUID PRIMARY KEY,
req_hash BYTEA NOT NULL,
status INT NOT NULL,
response JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expiry_at TIMESTAMPTZ NOT NULL
);
6. 5 Handler pseudocodul
pseudo handle_write(req):
k = req. headers["Idempotency-Key"]
h = hash(req. body)
rec = idempo_store. get(k)
if rec and rec. req_hash == h:
return rec. status, rec. response, {"Idempotency-Replay": "true"}
if rec and rec. req_hash!= h:
return 409, problem("IDEMPOTENT_CONFLICT")
begin tx result = apply_business_mutation (req) # change status upsert once (idempo_store, key = k, req_hash=h, status = 201, response = result, expiry = now () + 2d)
commit
return 201, result
7) modele „efectiv-o dată”
Outbox tranzacțional: înregistrarea unui eveniment de afaceri și trimiterea unui mesaj din aceeași tranzacție de baze de date prin intermediul releului de fundal; consumatorul este idempotent.
Inbox/Mese procesate la consumator: salvați 'event _ id' pentru a ignora duplicatele.
Exact o dată pe Kafka ≠ exact o dată în afaceri: chiar și cu producătorul/consumatorul EOS, logica aplicată ar trebui să fie în continuare idempotentă.
Compensarea tranzacțiilor (Saga): dacă pașii se retrag și provoacă efecte secundare, întoarcem sistemul la invariant.
8) Cazuri speciale: plăți și tranzacții financiare
Idempotență puternică: Cheia este legată de logica de operare (de ex. 'extern _ payment _ id').
Deduplicarea pe PSP - Store 'merchant _ reference' → dacă se repetă, PSP va returna același rezultat.
Retrage „de la client”: permite numai atunci când „Idempotency-Key”, în caz contrar riscul de dublă anulare.
Concurență: blochează „pe cont/instrument/contract” pe durata execuției; când se repetă, se întoarce 409/423.
Observabilitate: măsurători 'idempo _ relay _ total', 'idempo _ conflict _ total'.
9) Cârlige web și provocări externe
Semnături HMAC și fereastra de timp; prima verificare, apoi procesare.
Retribuirile expeditorului: backoff exponențial + jitter, 'max _ încercări' și DLQ.
Consumator - idempotent: 'event _ id' → tabel/memorie cache; ordinul „ordonat” nu este garantat.
Coduri: 2xx = succes, 4xx = nu se repetă, 5xx/timeout = repetare.
10) Cozi și sarcini de fundal
Cel puțin o dată în mod implicit → duplicatele sunt inevitabile.
Stocați 'task _ id'/' event _ id' și starea de execuție; cu duplicate - calea scurtă „reluare”.
DLQ și mesaje otrăvitoare: încercare contra, carantină, parsare manuală.
Limite competitive (semafoare) și lucrători idempotenți.
11) Chei de versioning și „naturale”
Cheile naturale (numărul contului + data + numărul documentului) cresc rezistența la repetiție.
Când schimbați schema/versiunea, includeți cheia versiunii în 'Idempotency-Key' sau în hash-ul interogării.
12) antete HTTP și solicită clientului
'Idempotency-Key', 'Idempotency-Replay', 'Retry-After', 'Prefer: wait = <sec>' (la operaţii lungi), 'If-Match '/' ETag' (încuietori optimiste).
409 pentru un conflict cheie 425/429/503 cu valid „Retry-After”.
Pentru operațiunile "lungi" - primirea statutului asincron ('202 Acceptat "+' Locație 'per resursă de stare).
13) Scenarii de testare și haos
Teste negative: trimiterea dublă, repetarea cu un alt corp, desincronizarea ceasului.
Out of order: „t2” vine înainte de „t1”.
Injectarea de termene/„ RST ”/„ EOF ”, jumătate de solicitări (lent-POST).
Stocarea idempotenței decăzute → comportamentul închis (eșec mai bun decât dubla eliminare).
14) Măsurători și alerte
'retries _ total {reason}', 'retry _ budget _ used {route}', 'backoff _ seconds _ bucket'.
'idempo _ relay _ total', 'idempo _ conflict _ total', 'duplicate _ detected _ total'.
Share 409/425/429/5xx pe rute; p95/p99 „timp pentru succes” cu retrageri.
Alerte: buget de recuperare a ratei de ardere, creștere a conflictelor de idempotență, creștere DLQ.
15) Antipattern
Retrage toate greşelile la rând.
Lipsa de jitter → unde sincrone de retrasări.
Cheile de lungă durată fără TTL și de curățare.
Salvarea rezultatului după un efect secundar comite (încălcarea outbox).
Jurnalele fără 'trace _ id'/' idempotency _ key' sunt → imposibil de generat.
Retroadele paralele agresive la operațiile de scriere.
16) Lista de verificare Prod Readiness
- Politica unificată: ce retraim, ce nu; coduri și solicitări pentru clienți.
- Backoff exponențial + jitter complet; „retry _ budget” specificat.
- Contract 'Idempotency-Key' + stocarea rezultatelor cu TTL.
- Outbox/Inbox pentru evenimente; DLQ; limite competitive.
- Integrarea cu întrerupător de circuit, respect „Retry-After”.
- Metrics/Alerts by Retray/Duplicate/Conflict.
- Un set de teste de haos și emularea eșecului rețelei.
- Documentația clientului - exemple de backup-uri și stări.
17) TL; DR
Retragerile sunt utile doar împreună cu idempotența. Introduceți „Idempotency-Key” și stocarea rezultatelor, aplicați backoff exponențial cu jitter și reîncercați-buget, respectați „Retry-After”, integrați-vă cu întrerupătorul de circuit. Pentru evenimente - outbox/inbox; pentru plăți, eliminare strictă a duplicatelor și încuietori. Măsurați retraiele și conflictele, testați duplicatele și timeouturile.