Idempotens i distribuerade system som faktiskt fungerar

Avbryt dubbla sideffekter

Sidinnehåll

Idempotens i distribuerade system är den egenskap som räddar dig när nätverket ljuger, köen gör om försök, klienten paniker och operatören spelar om händelser. I produktionsmiljöer är dubbelleverans normal. Dubbla biverkningar är buggen.

HTTP definierar en idempotent metod som en där flera identiska förfrågningar har samma avsedda effekt på servern som en enda förfrågan. Därför är PUT, DELETE och säkra metoder idempotenta enligt protokollsemantik och kan göras om automatiskt efter ett kommunikationsfel.

integrationsmeddelandeflöde: idempotens

Den definitionen är användbar, men den räcker inte. I verkliga arkitekturer är idempotens inte ett trivia-svar om HTTP. Det är en affärs garanti. Om en kund trycker på “betala” en gång, får du inte debitera två gånger för att en timeout inträffade mellan åtagande och svar. Om en worker uppdaterar lagret och kraschar innan den bekräftar meddelandet, får du inte minska lagret två gånger för att mäklaren levererade meddelandet igen. Det är måttet.

Mistaken som jag ser gång på gång är att behandla idempotens som en transportfunktion istället för en systemegenskap. Ködeduplicering, HTTP-verb och klientförsök hjälper till, men ingen av dem räddar en design som låter samma affärsintent skapa en andra biverkning. Om du vill ha den bredare ramen för hur dessa integrationsbeslut passar servicegränser och uthållighetsavvägningar, börja med App-arkitektur i produktion: Integrationsmönster, koddesign och dataåtkomst.

Var duplicat kommer ifrån i produktion

Duplicat dyker inte upp för att team är slarviga. De dyker upp för att distribuerade system gör om, omordnar och spelar om.

En klient kan skicka en skapningsförfrågan, servern kan åtaga den, och svaret kan fortfarande försvinna på vägen. Det är just därför HTTP distingerar idempotenta metoder och varför betalnings-API:er som Stripe och PayPal exponerar explicita idempotensmekanismer för osäkra metoder som POST.

Meddelandemäklare gör problemet ännu mer uppenbart. Leverans “at-least-once” betyder att en konsument kan anropas upprepade gånger för samma meddelande, och en hanterare kan uppdatera databasen framgångsrikt men misslyckas innan bekräftelse, vilket får mäklaren att leverera samma meddelande igen.

Webhooks är inget undantag. GitHub säger att webhookleveranser kan komma i fel ordning, misslyckade leveranser levereras inte automatiskt om, och varje leverans bär på en unik X-GitHub-Delivery GUID som du bör använda när du skyddar mot uppspelning. För en praktisk arkitektursyn på chattändpunkter som interaktionsgränser, se Chattplattformar som systemgränssnitt i moderna system.

Även system som reklamerar starkare garantier lämnar fortfarande dig med arbete att göra. Kafka kan förhindra dubbla poster i Kafka-loggar med idempotenta producenter och kan erbjuda “exactly-once”-leverans för läs-process-skriv-flöden som stannar inom Kafka med transaktioner och read_committed-konsumenter. Men Kafkas egna design-dokument är tydliga med att externa system fortfarande kräver koordinering med offset och utdata. Google Cloud Pub/Subs “exactly-once”-leverans är begränsad till dragprenumerationer, inom en molnregion, och kräver fortfarande att klienter spårar behandlingsframsteg tills bekräftelse lyckas.

Min opinionerade sammanfattning är enkel. Anta att transporten kommer att göra om. Anta att operatörer kommer att spela om. Anta att webhooks kommer sent. Designa skrivvägen så att en upprepad intent inte kan skapa en andra affärseffekt.

Det API-avtal jag faktiskt litar på

Hur idempotensnycklar förhindrar dubbla API-förfrågningar

Det enda API-avtal jag litar på för muterande operationer är kallansuppgiven intent plus serverbaserad uthållighet.

AWS rekommenderar en kallsuppgiven förfrågningsidentifierare och varnar för att tjänsten måste atomiskt registrera idempotens-token tillsammans med det muterande arbetet. Stripe lagrar den första statuskoden och svarsbrödtexten för en nyckel, jämför senare parametrar med den ursprungliga förfrågan och returnerar samma resultat för omförsök. PayPal använder PayPal-Request-Id på stödda POST-API:er och returnerar senaste status för den tidigare förfrågan med samma header.

Det leder till ett praktiskt avtal:

  1. Klienten genererar en idempotensnyckel för en affärsoperation.
  2. Servern scoper den nyckel efter hyresgäst och operationsnamn.
  3. Servern lagrar en förfrågningshash så att samma nyckel inte kan återanvändas för en annan payload.
  4. Servern registrerar status som pending, completed eller failed.
  5. Omförsök med samma nyckel returnerar antingen det lagrade resultatet eller en stabil pekare till det.
  6. Omförsök med samma nyckel och en annan payload misslyckas tydligt.

Det finns ett IETF-utkast till Idempotency-Key-header, men per 2026-05-09 listas det fortfarande i IETF Datatracker som ett utgånget Internet-Draft snarare än en publicerad RFC. I praktiken är headernamnet fortfarande brett användbart som en de facto-konvention, men du bör dokumentera avtalet i ditt eget API istället för att låtsas att standarden är färdig.

Vad ska nyckeln representera? Intent. Inte ett HTTP-försök. Inte en TCP-anslutning. Inte en omförsökssräknare. Om användaren menar “skapa order 123 en gång”, måste varje omförsök för samma kommando återanvända samma nyckel. Om användaren menar “placera en andra order”, måste det använda en annan nyckel.

En förfrågnings-ID är för spårning. En idempotensnyckel är för korrekthet. Om du blandar ihop dem, ser dina dashboards snygga ut medan dina pengar flyttas två gånger.

Varför PUT inte räcker

Nej, HTTP PUT räcker inte för att göra en operation idempotent.

Ja, RFC 9110 ger PUT idempotent semantik. Men om din PUT-hanterare emitterar en ny nedströms händelse, skickar ett e-postmeddelande vid varje omförsök eller debiterar en extern leverantör igen, har din implementering brutit mot affärsavtalet även om ditt routnamn ser respektabelt ut.

Val av verb hjälper klienter att förstå intent. Det implementerar inte intent åt dig.

Använd PUT när resursmodellen verkligen passar en fullständig ersättning eller upsert-stil operation. Använd POST när du skapar kommandon eller åtgärder. Men för någon mutation som kan göras om över nätverksgränser, dokumentera ett explicit idempotensavtal. Om dina muterande åtgärder utlöses från chattarbetsflöden, gäller samma avtal i Slack-integrationsmönster för alarmer och arbetsflöden och Discord-integrationsmönster för alarmer och kontrolllopp. Dolda biverkningar är där arkitektur går för att dö.

Hur länge bör en idempotensnyckel lagras

Längre än ditt transportteam vill.

Stripe säger att nycklar kan klyvas efter minst 24 timmar. PayPal säger att retention är API-specifik och ger exempel som kan varaade upp till 45 dagar. Amazon SQS FIFO deduplicerar endast inom ett 5-minuters fönster. GitHub behåller senaste leveranser i 3 dagar för manuell omleverans. Dessa siffror är vilt olika för att rätt retentionstid är ett affärsbeslut, inte ett protokollstandardvärde.

Om du bara behåller nycklar i fem minuter för att din kö gör det, designar du inte idempotens. Du kopierar en transportbegränsning in i din affärslager.

Behåll idempotensregister i minst maximum av dessa fönster:

  • klientens omförsökshorison
  • köns omleveranshorison
  • webhookens uppspelningshorison
  • operatörens uppspelningshorison
  • avstämnings- eller kompenseringshorison för pengar-flyttande operationer

För betalningar, bokningar och provisioning betyder det ofta timmar eller dagar, inte minuter.

AWS påpekar också två anti-mönster som jag helt håller med om. Använd inte tidsstämplar som nyckel, eftersom klockskew och kollisioner gör dem opålitliga. Lagra inte blindt hela förfrågningspayload som dedup-registrering för varje förfrågan, eftersom det skadar prestanda och skalbarhet. Lagra en normaliserad förfrågningshash plus den minimala svarsstatus du behöver för att spela om säkert. Om du måste reproducera det första svaret byte för byte, lagrar den kanoniska svarsbrödtexten som Stripe gör.

Databasmönster som gör idempotens verklig

Idempotens blir verklig när uthållighetslagret kan vinna en race exakt en gång.

PostgreSQL ger dig två kritiska primitiver här. Unika begränsningar tillämpar unikhet på en eller flera kolumner, och INSERT ... ON CONFLICT låter dig definiera en alternativ åtgärd istället för att misslyckas vid en unikhetsöverträdelse. PostgreSQL dokumenterar också att ON CONFLICT DO UPDATE garanterar ett atomisk insert-eller-uppdate-resultat under konkurrens.

Det betyder att ditt idempotenslager vanligtvis bör börja med en tabell som denna:

create table api_idempotency (
    tenant_id text not null,
    operation text not null,
    idempotency_key text not null,
    request_hash text not null,
    state text not null,
    status_code integer,
    response_body jsonb,
    resource_type text,
    resource_id text,
    created_at timestamptz not null default now(),
    expires_at timestamptz not null,
    primary key (tenant_id, operation, idempotency_key)
);

Och hanteringsflödet bör se ut så här:

begin transaction

try insert (tenant_id, operation, idempotency_key, request_hash, state='pending')
on conflict do nothing

load row for (tenant_id, operation, idempotency_key) for update

if row.request_hash != incoming_request_hash
    fail with conflict or validation error

if row.state = 'completed'
    return stored response

if row.state = 'pending' and row was created by another live request
    either wait briefly, or fail fast with a retryable response

perform local business mutation

store stable result in idempotency row
set state = 'completed'

commit
return result

Den viktiga delen är inte syntaxen. Den viktiga delen är atomiciteten. Att registrera nyckeln och utföra mutationen måste lyckas eller misslyckas tillsammans. AWS säger detta explicit för API-idempotens, och samma regel gäller i SQL-baserade tjänster.

Gör inte en naiv check-then-act-sekvens som “select key; if missing then insert order”. Under konkurrens kan två förfrågningar passera kontrollen och båda skapa biverkningen. En unik begränsning är inte valfri. Det är mekanismen som transformerar din arkitektur från optimistisk folktro till något du kan bevisa under last.

Här är regeln jag använder i granskningar. Om dedup-beslutet inte skyddas av samma transaktionsgräns som mutationen, har du inte idempotens. Du har hopp.

Meddelanden, händelser och webhooks behöver sina egna gränser

Hur konsumenter hanterar dubbla händelser och meddelanden

För meddelandekonsumenter är det klassiska mönstret fortfarande det rätta. Registrera processade meddelande-ID:n i samma databastransaktion som affärsuppdateringen. Chris Richardson beskriver PROCESSED_MESSAGES-tabellapprochen direkt, med en primär nyckel på prenumerant och meddelande-ID så att duplicat misslyckas rent och kan ignoreras.

Många team kallar det explicita processed_messages-lagret för en inbox-tabell. Etiketten är mindre viktig än regeln. Mottagaren måste uthålligt bevisa att den redan hanterade meddelandet innan ett omförsök säkert kan göra ingenting.

En minimal form ser ut så här:

create table processed_messages (
    subscriber_id text not null,
    message_id text not null,
    processed_at timestamptz not null default now(),
    primary key (subscriber_id, message_id)
);

Och konsumentflödet är lika strikt som HTTP-flödet:

begin transaction

insert into processed_messages (subscriber_id, message_id)
values (?, ?)
on conflict do nothing

if no row inserted
    rollback
    ack and ignore duplicate

apply business mutation

commit
ack message

Det mönstret är tråkigt. Bra. Idempotens bör vara tråkig.

Det är också oftast bättre än att försöka luta sig mot mäklarmarknadstermer. Kafkas “exactly-once”-stöd är utmärkt när du stannar inom Kafkas eget transaktionsmodell, men Kafkas docs varnar fortfarande för att externa destinationer behöver samarbete. SQS FIFO reducerar dubbla sändningar endast inom sitt 5-minuters dedup-fönster. Pub/Sub “exactly-once” förväntar sig fortfarande att prenumeranten spårar framsteg och undviker dubbelarbete när bekräftelser misslyckas.

“Exactly-once” är vanligtvis en lokal optimering. Idempotenta biverkningar är systemgarantin.

Kombinera dedup med outbox-mönstret

Om din tjänst uppdaterar lokal status och också publicerar en händelse, räcker idempotent konsumtion inte. Du behöver också ett säkert sätt att få ut händelsen efter den lokala transaktionen åtagits.

Det är därför det transaktionella outbox-mönstret är viktigt. Chris Richardson beskriver den grundidén som att skriva händelsen till en outbox-tabell i samma transaktion som affärsuppdateringen, och sedan publicera den asynkront. Debezium säger att outbox-mönstret undviker inkonsekvenser mellan en tjänsts interna status och händelserna som konsumeras av andra tjänster. NServiceBus går längre och visar hur outbox-behandling deduplicerar inkommande meddelanden och undviker zombieregistreringar och spöksmeddelanden.

Detta är arkitekturen jag rekommenderar för tjänster som äger data och publicerar integrationshändelser:

  1. Validera och uthåll kommandot under en idempotensnyckel.
  2. Skriv affärsstatus och outbox-händelse i en lokal transaktion.
  3. Låt CDC eller en outbox-dispatcher publicera händelsen.
  4. Gör nedströmskonsumenter idempotenta också.

Outbox tar inte bort behovet av idempotenta konsumenter. Det tar bort behovet av att låtsas att ett databasåtagande och en mäklarpublikation kan vara en magisk distribuerad transaktion när de oftast inte kan.

Webhooks är bara meddelanden med bättre varumärke

Bevaka inkommande webhooks exakt som meddelanden från en okänd nätverkskant.

GitHub dokumenterar att leveranser kan komma i fel ordning, rekommenderar att använda X-Hub-Signature-256 för att verifiera äkthet, och tillhandahåller X-GitHub-Delivery som den unika leveransidentifieraren. Den noterar också att omleveranser återanvänder samma leverans-ID.

Så arkitekturen är rakt fram:

  • verifiera signaturen först
  • använd leverans-GUID som dedup-nyckel
  • uthåll mottagning innan biverkningar
  • gör hanterare ordningsmedvetna snarare än att anta leveransordning
  • köa det tunga arbetet och returnera snabbt

Om din webhook-hanterare skriver direkt till affärstabeller innan den registrerar mottagning, är den inte produktionsklar. Den är bara snabbare på att göra dubbla misstag.

Sagor och arbetsflödesmotorer behöver fortfarande idempotens

Sagor och hållbara arbetsflödesmotorer tar inte bort problemet. De gör det synligt.

Temporal rekommenderar att skriva Activities för att vara idempotenta eftersom Activities kan göras om efter fel eller timeouts. Dess docs pekar till och med ut grannfallet där en worker fullbordar en extern biverkning framgångsrikt men kraschar innan den rapporterar fullbordande, vilket får Activity att köras igen. Temporal föreslår också att använda en kombination av Workflow Run ID och Activity ID som en stabil idempotensnyckel vid anrop till nedströmstjänster. Om du tillämpar detta i serviceorkestrering, täcker Go Microservices för AI/ML Orkestrering de bredare arbetsflödesavvägningarna.

Det är exakt rätt mentala modell. En arbetsflödesmotor kan bevara utförandehistorik och koordinera omförsök. Den kan inte retroaktivt återdebitera ett kort eller återsända ett e-postmeddelande om inte din application ger den idempotenta steg och idempotenta kompensationer.

Samma gäller för sagor. Temporals egen sagoguidance beskriver kompenserande åtgärder som körs när ett steg misslyckas. Dessa kompensationer måste också vara idempotenta. Om “återbetal betalning” körs två gånger, kan du ha löst den ursprungliga buggen genom att skapa en ny.

Min regel här är brutal och enkel. Varje Activity, varje kommandohanterare, och varje kompensation som rör den yttre världen bör antingen vara naturligt idempotent eller bära en verklig idempotensnyckel till nedströmssystemet.

Hur man testar idempotens innan produktion

De flesta team testar lyckliga vägar och blir sedan överraskade när omförsök händer. Det är inte tillräckligt.

Du bör ha automatiserade tester för åtminstone dessa fall:

  • servern åtar sig mutationen men svaret når aldrig klienten
  • två identiska förfrågningar tävlar med samma idempotensnyckel
  • samma nyckel återanvänds med en annan payload
  • en konsument åtar sig sitt databasarbetet och kraschar innan ack
  • en webhook spelas om med samma leverans-ID
  • en outbox-dispatcher publicerar samma händelse mer än en gång
  • en workflow Activity fullbordar det externa samtalet och kraschar innan fullbordande rapporteras
  • en idempotensregistrering löper ut och ett äkta sent omförsök kommer

AWS rekommenderar explicit omfattande testsuites som inkluderar lyckade förfrågningar, misslyckade förfrågningar och dubbla förfrågningar. Det rådet är vardagligt och absolut korrekt.

Jag skulle lägga till ytterligare ett felövning. Verifiera att det spelade om svaret är semantiskt ekvivalent med det första resultatet. AWS diskuterar sent ankommande omförsök och argumenterar för svar som bevarar den ursprungliga meningen även efter att underliggande status har ändrats. Det är skillnaden mellan “ingen extra biverkning hände” och “kallaren fortfarande har ett konsistent avtal.”

Opinionerade regler som räddar verkliga system

Här är reglerna jag skulle tillämpa i en arkitekturkanalys.

Först, idempotensnycklar tillhör affärsintent, inte transportförsök.

För det andra, scopa varje nyckel efter hyresgäst och operation. Globala nyckelutrymmen är hur orelaterade förfrågningar kolliderar.

För det tredje, uthåll dedup-beslutet atomiskt med mutationen. Om det inte är sant, är designen fel.

För det fjärde, avvisa omförsök med samma nyckel och annan payload. Stripe och AWS gör detta av goda skäl.

För det femte, behåll nycklar för hela uppspelningshorisonten för affärsprocessen, inte för det kortaste köfönstret.

För det sjätte, para producenter med en outbox och konsumenter med meddelande-ID-spårning. En sida utan den andra är halva en design.

För det sjunde, propagera samma operationsidentitet nedströms när affärsåtgärden är densamma. AWS rekommenderar explicit att passera idempotens-token längs bearbetningskedjan.

För det åttonde, anta aldrig att “exactly-once”-marknadsföring tar bort behovet av idempotenta biverkningar.

Om det låter strikt, bra. Idempotens är där optimistisk arkitektur möter produktionsrealitet. Du behöver inte komplexitet överallt. Men var dubbla biverkningar skulle skada pengar, status eller förtroende, bör idempotens vara en förstaklassdel av avtalet.

Användbara länkar

Prenumerera

Få nya inlägg om system, infrastruktur och AI-ingenjörskonst.