Idempotentność w rozproszonych systemach, która naprawdę działa
Zatrzymaj duplikujące efekty uboczne
Idempotentność w rozproszonych systemach to cecha, która ratuje Cię, gdy sieć kłamie, kolejka ponawia próby, klient panikuje, a operator wciska odtwarzanie. W systemach produkcyjnych wielokrotne dostarczanie danych jest normalne. Wielokrotne efekty uboczne to błąd.
HTTP definiuje metodę jako idempotentną, gdy wiele identycznych żądań ma taki sam zamierzony skutek po stronie serwera jak jedno żądanie. Dlatego PUT, DELETE i metody bezpieczne są idempotentne w semantyce protokołu i mogą być automatycznie ponawiane po awarii komunikacji.

Ta definicja jest przydatna, ale niewystarczająca. W rzeczywistych architekturach idempotentność nie jest trywialnym pytaniem z zakresu HTTP. To gwarancja biznesowa. Jeśli klient kliknie „zapłać” raz, nie możesz obciążyć go dwukrotnie z powodu timeoutu między zatwierdzeniem a odpowiedzią. Jeśli pracownik zaktualizuje zapasy i ulegnie awarii przed potwierdzeniem (ack) wiadomości, nie możesz dwukrotnie zmniejszyć stanu magazynowego tylko dlatego, że broker ponownie dostarczył tę samą wiadomość. To jest poprzeczka.
Błąd, który widzę powtarzający się od wielokrotności, to traktowanie idempotentności jako cechy warstwy transportowej, a nie właściwości systemu. Deduplikacja w kolejce, metody HTTP i ponawianie próśb przez klienta pomagają, ale żaden z nich nie uratuje projektu, który pozwala na wywołanie drugiego efektu ubocznego dla tej samej intencji biznesowej. Jeśli chcesz uzyskać szersze ramy dotyczące tego, jak te decyzje integracyjne wpisują się w granice usług i kompromisy w zakresie trwałości danych, zacznij od App Architecture in Production: Integration Patterns, Code Design, and Data Access.
Skąd biorą się duplikaty w środowisku produkcyjnym
Duplikaty nie pojawiają się dlatego, że zespoły są nieostrożne. Pojawiają się, ponieważ rozproszone systemy ponawiają próby, zmieniają kolejność i odtwarzają zdarzenia.
Klient może wysłać żądanie utworzenia, serwer może je zatwierdzić, a odpowiedź nadal może zaginąć w sieci. To dokładnie dlatego HTTP rozróżnia metody idempotentne i dlaczego API płatności takie jak Stripe czy PayPal udostępniają jawne mechanizmy idempotentności dla metod niebezpiecznych, takich jak POST.
Brokery wiadomości jeszcze bardziej uwidaczniają problem. Dostawa „co najmniej raz” oznacza, że konsument może być wywoływany wielokrotnie dla tej samej wiadomości, a handler może pomyślnie zaktualizować bazę danych, ale zawieść przed potwierdzeniem, co spowoduje, że broker ponownie dostarczy tę samą wiadomość.
Webhooki nie są inaczej. GitHub mówi, że dostawy webhooków mogą pojawiać się w niepoprawnej kolejności, niepowodzone dostawy nie są automatycznie ponawiane, a każda dostawa zawiera unikalny identyfikator X-GitHub-Delivery GUID, którego należy używać przy ochronie przed odtwarzaniem. Aby uzyskać praktyczny widok architektury punktów końcowych czatu jako granic interakcji, zobacz Chat Platforms as System Interfaces in Modern Systems.
Nawet systemy, które reklamują silniejsze gwarancje, wciąż wymagają od Ciebie wykonania pracy. Kafka może zapobiegać duplikatom wpisów w logach Kafka za pomocą producerów idempotentnych i może zapewniać dostawę dokładnie raz dla przepływów read-process-write, które pozostają wewnątrz Kafka przy użyciu transakcji i konsumentów read_committed. Jednak dokumenty projektowe samej Kafka jasno mówią, że systemy zewnętrzne nadal wymagają koordynacji z offsetami i wynikami. Dostawa dokładnie raz w Google Cloud Pub/Sub jest ograniczona do subskrypcji pull, w obrębie regionu chmury i nadal wymaga od klientów śledzenia postępu przetwarzania do momentu pomyślnego potwierdzenia.
Moja subiektywna podsumowanie jest proste. Załóż, że transport będzie ponawiał próby. Załóż, że operatorzy będą odtwarzali zdarzenia. Załóż, że webhooki będą przychodzić z opóźnieniem. Zaprojektuj ścieżkę zapisu tak, aby powtórzona intencja nie mogła stworzyć drugiego efektu biznesowego. Projektowanie błędów jest ściśle powiązane: sposób, w jaki błędy są opakowywane, tłumaczone i klasyfikowane jako ponawialne versus nieponawialne, jest częścią tej samej dyscypliny granic — Go Error Handling Architecture: Boundaries and Patterns omawia klasyfikację błędów ponawialnych, tłumaczenie granic i wzorce sentinelowe, które pozwalają logice ponawiania podejmować trafne decyzje. Gdy ponawianie próbie nadal napotyka niezdrową zależność, circuit breaker na granicy integracji zawodzi szybko, zanim burze ponawianek pomnożą zduplikowaną pracę.
Umowa API, której naprawdę ufam
Jak klucze idempotentności zapobiegają duplikatowi żądań API
Jedyną umową API, której ufam w przypadku operacji modyfikujących, jest intencja dostarczana przez wywołującego plus trwałość po stronie serwera.
AWS zaleca dostarczenie identyfikatora żądania przez wywołującego i ostrzega, że usługa musi atomowo zapisać token idempotentności wraz z pracą modyfikującą. Stripe przechowuje pierwszy kod stanu i ciało odpowiedzi dla klucza, porównuje późniejsze parametry z oryginalnym żądaniem i zwraca ten sam wynik dla ponawianych prób. PayPal używa nagłówka PayPal-Request-Id w obsługiwanych interfejsach POST API i zwraca najnowszy status dla poprzedniego żądania z tym samym nagłówkiem.
To prowadzi do praktycznej umowy:
- Klient generuje klucz idempotentny dla operacji biznesowej.
- Serwis scope’uje ten klucz według tenant’a i nazwy operacji.
- Serwis przechowuje hash żądania, aby ten sam klucz nie mógł być ponownie użyty dla innego payloadu.
- Serwis zapisuje stan taki jak
pending,completedlubfailed. - Ponawiane próby z tym samym kluczem zwracają albo zapisany wynik, albo stabilny wskaźnik do niego.
- Ponawiane próby z tym samym kluczem i innym payloadem kończą się jawnym błędem.
Istnieje projekt standardu IETF Idempotency-Key, ale stan na 09.05.2026 nadal jest wymieniany w IETF Datatracker jako wygasły Internet-Draft zamiast opublikowanego RFC. W praktyce nazwa nagłówka jest nadal szeroko użyteczna jako konwencja de facto, ale powinieneś udokumentować umowę we własnym API, a nie udawać, że standard jest zakończony.
Co powinien reprezentować klucz? Intencję. Nie próbę HTTP. Nie połączenie TCP. Nie licznik ponawiania. Jeśli użytkownik ma na myśli „utwórz zamówienie 123 raz”, każde ponowienie tego samego polecenia musi ponownie użyć tego samego klucza. Jeśli użytkownik ma na myśli „złóż drugie zamówienie”, musi użyć innego klucza.
ID żądania służy do śledzenia. Klucz idempotentny służy do poprawności. Jeśli je pomieszasz, Twoje dashboardy wyglądają schludnie, podczas gdy Twoje pieniądze przemieszczają się dwukrotnie.
Dlaczego PUT to za mało
Nie, HTTP PUT nie wystarczy, aby uczynić operację idempotentną.
Tak, RFC 9110 nadaje PUT semantyce idempotentnej. Ale jeśli Twój handler PUT emituje nowe zdarzenie downstream, wysyła e-mail przy każdej ponowionej próbie lub ponownie obciąża zewnętrznego dostawcę, wtedy Twoja implementacja złamała umowę biznesową, nawet jeśli nazwa trasy wygląda godnie.
Wybór czasownika pomaga klientom zrozumieć intencję. Nie implementuje jednak intencji za Ciebie.
Używaj PUT, gdy model zasoru naprawdę pasuje do operacji pełnej zamiany lub upsert. Używaj POST, gdy tworzysz polecenia lub akcje. Ale w przypadku każdej modyfikacji, która może być ponawiana w obrębie granic sieci, udokumentuj jawną umowę idempotentności. Jeśli Twoje akcje modyfikujące są wyzwalane z przepływów czatu, ta sama umowa ma zastosowanie w Slack Integration Patterns for Alerts and Workflows oraz Discord Integration Pattern for Alerts and Control Loops. Ukryte efekty uboczne są miejscem, gdzie architektura umiera.
Jak długo należy przechowywać klucz idempotentny
Dłużej niż Twój zespół transportowy chce.
Stripe mówi, że klucze można usuwać po co najmniej 24 godzinach. PayPal mówi, że retencja jest specyficzna dla API i podaje przykłady, które mogą trwać do 45 dni. Amazon SQS FIFO deduplikuje tylko w obrębie okna 5-minutowego. GitHub przechowuje niedawne dostawy przez 3 dni do ręcznego ponowienia. Te liczby są diametralnie różne, ponieważ właściwy okres retencji to decyzja biznesowa, a nie domyślna wartość protokołu.
Jeśli przechowujesz klucze tylko przez pięć minut, ponieważ tak robi Twoja kolejka, nie projektujesz idempotentności. Kopiujesz ograniczenie transportu do warstwy biznesowej.
Przechowuj rekordy idempotentności przez co najmniej maksymalny z tych okien:
- horyzont ponawiania przez klienta
- horyzont ponownego odczytu kolejki
- horyzont odtwarzania webhooków
- horyzont odtwarzania przez operatora
- horyzont rozliczeń lub kompensacji dla operacji przenoszących pieniądze
W przypadku płatności, rezerwacji i dostarczania usług często oznacza to godziny lub dni, a nie minuty.
AWS wskazuje również dwa antywzorce, z którymi w pełni się zgadzam. Nie używaj znaczników czasu jako klucza, ponieważ przesunięcie zegara i kolizje czynią je niezawodnymi. Nie przechowuj ślepo całych payloadów żądania jako rekordu deduplikacji dla każdego żądania, ponieważ szkodzi to wydajności i skalowalności. Przechowuj znormalizowany hash żądania plus minimalny stan odpowiedzi potrzebny do bezpiecznego odtworzenia. Jeśli musisz odtworzyć pierwszy bajt odpowiedzi byte-for-byte, przechowuj kanoniczne ciało odpowiedzi w sposób, w jaki robi to Stripe.
Wzorce bazy danych, które czynią idempotentność rzeczywistą
Idempotentność staje się rzeczywista, gdy warstwa trwałości może wygrać wyścig dokładnie raz.
PostgreSQL daje Ci dwie krytyczne funkcje tu. Ograniczenia unikalności wymuszają unikalność na jednej lub więcej kolumnach, a INSERT ... ON CONFLICT pozwala zdefiniować alternatywną akcję zamiast awarii przy naruszeniu unikalności. PostgreSQL dokumentuje również, że ON CONFLICT DO UPDATE gwarantuje atomowy wynik wstawiania-aktualizacji przy współbieżności.
Oznacza to, że Twoja warstwa idempotentności powinna zwykle zaczynać się od tabeli takiej jak ta:
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)
);
A przepływ obsługi powinien wyglądać tak:
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
Ważna część nie leży w składni. Ważna część to atomowość. Zapisanie klucza i wykonanie modyfikacji muszą się powieść lub zawieść razem. AWS mówi o tym wprost w kontekście idempotentności API, a ta sama zasada ma zastosowanie w usługach opartych na SQL.
Nie rób naiwnej sekwencji sprawdź-następnie-akcja takiej jak „select key; if missing then insert order”. Przy współbieżności dwa żądania mogą przejść sprawdzenie i oba stworzą efekt uboczny. Ograniczenie unikalne nie jest opcjonalne. Jest mechanizmem, który zamienia Twoją architekturę z optymistycznej legendy w coś, co możesz udowodnić pod obciążeniem.
Oto zasada, której używam w przeglądach. Jeśli decyzja deduplikacji nie jest chroniona przez tę samą granicę transakcyjną co modyfikacja, nie masz idempotentności. Masz nadzieję.
Komunikaty, zdarzenia i webhooki potrzebują własnej granicy
Jak konsumenci obsługują duplikaty zdarzeń i wiadomości
W przypadku konsumentów wiadomości klasycznym wzorcem jest wciąż ten właściwy. Zapisz przetworzone identyfikatory wiadomości w tej samej transakcji bazodanowej co aktualizację biznesową. Chris Richardson opisuje podejście z tabelą PROCESSED_MESSAGES wprost, używając klucza głównego na subskrybencie i identyfikatorze wiadomości, dzięki czemu duplikaty kończą się czysto i mogą być ignorowane.
Wiele zespołów nazywa ten jawny magazyn processed_messages tabelą skrzynki odbiorczej (inbox). Etykieta ma mniejsze znaczenie niż reguła. Odbiorca musi utrwal dowód, że już obsłużył wiadomość, zanim ponowienie może bezpiecznie nic nie robić.
Minimalna forma wygląda tak:
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)
);
A przepływ konsumenta jest tak samo rygorystyczny jak przepływ HTTP:
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
Ten wzorzec jest nudny. Dobrze. Idempotentność powinna być nudna.
Zazwyczaj jest też lepsza niż próba polegania na marketingowych terminach brokerów. Exactly-once w Kafka jest doskonały, gdy pozostajesz wewnątrz własnego modelu transakcyjnego Kafka, ale dokumentacja Kafka nadal ostrzega, że zewnętrzne punkty docelowe wymagają współpracy. SQS FIFO redukuje duplikaty wysyłek tylko w obrębie swojego 5-minutowego okna deduplikacji. Exactly-once w Pub/Sub nadal oczekuje, że subskrybent będzie śledził postępy i unikał zduplikowanej pracy, gdy potwierdzenia zawiodą.
Exactly-once to zwykle optymalizacja lokalna. Idempotentne efekty uboczne to gwarancja systemowa.
Połącz deduplikację z wzorcem outbox
Jeśli Twoja usługa aktualizuje lokalny stan i również publikuje zdarzenie, sama idempotentna konsumpcja nie wystarczy. Potrzebujesz także bezpiecznego sposobu na dostarczenie zdarzenia po zatwierdzeniu lokalnej transakcji.
Dlatego wzorzec transactional outbox ma znaczenie. Chris Richardson opisuje podstawowy pomysł jako zapisanie zdarzenia w tabeli outbox w tej samej transakcji co aktualizacja biznesowa, a następnie publikowanie go asynchronicznie. Debezium mówi, że wzorzec outbox zapobiega niespójnościom między stanem wewnętrznym usługi a zdarzeniami spożywanymi przez inne usługi. NServiceBus idzie dalej i pokazuje, jak przetwarzanie outbox deduplikuje przychodzące wiadomości i unika rekordów zombie oraz duchowych wiadomości.
To jest architektura, którą rekomenduję dla usług posiadających dane i publikujących zdarzenia integracyjne:
- Zwaliduj i utrwal polecenie pod kluczem idempotentnym.
- Zapisz stan biznesowy i zdarzenie outbox w jednej lokalnej transakcji.
- Pozwól CDC lub dispatcherowi outbox opublikować zdarzenie.
- Upewnij się, że konsumenci downstream są również idempotentni.
Outbox nie usuwa potrzeby posiadania idempotentnych konsumentów. Usuwa potrzebę udawania, że commit bazy danych i publikacja w brokerze mogą być jedną magiczną transakcją rozprozoną, gdy zwykle nie mogą.
Webhooki to po prostu wiadomości z lepszym brandingiem
Traktuj inbound webhooki dokładnie jak wiadomości z niezaufanego brzegu sieci.
GitHub dokumentuje, że dostawy mogą przychodzić w niepoprawnej kolejności, zaleca używanie X-Hub-Signature-256 do weryfikacji autentyczności i dostarcza X-GitHub-Delivery jako unikalny identyfikator dostawy. Wskazuje również, że ponowienia używają tego samego identyfikatora dostawy.
W związku z tym architektura jest prosta:
- najpierw zweryfikuj podpis
- użyj GUID dostawy jako klucza deduplikacji
- utrwal potwierdzenie przed efektami ubocznymi
- spraw, aby handlere byli świadomi kolejności, a nie zakładali kolejność przybycia
- enqueue ciężką pracę i zwracaj szybko
Jeśli Twój handler webhooka zapisuje bezpośrednio do tabel biznesowych przed zapisaniem potwierdzenia, nie jest gotowy do produkcji. Jest po prostu szybszy w robieniu duplikatów błędów.
Sagas i silniki workflow wciąż potrzebują idempotentności
Sagas i trwałe silniki workflow nie usuwają problemu. Ujawniają go.
Temporal zaleca pisanie Aktywności tak, aby były idempotentne, ponieważ Aktywności mogą być ponawiane po awariach lub timeoutach. Jego dokumentacja nawet wskazuje przypadek brzegowy, w którym pracownik pomyślnie wykonuje zewnętrzny efekt uboczny, ale ulega awarii przed zgłoszeniem zakończenia, co powoduje ponowne uruchomienie Aktywności. Temporal sugeruje również używanie kombinacji Workflow Run ID i Activity ID jako stabilnego klucza idempotentnego przy wywoływaniu usług downstream. Jeśli stosujesz to w orkiestracji usług, Go Microservices for AI/ML Orchestration omawia szersze kompromisy workflow.
To jest dokładnie właściwy model mentalny. Silnik workflow może zachować historię wykonania i koordynować ponawianie. Nie może cofnąć obciążenia karty lub cofnięcia wysłanego e-maila, chyba że Twoja aplikacja dostarczy mu idempotentnych kroków i idempotentnych kompensacji.
To samo dotyczy sag. Wskazówki sag Temporal opisują działania kompensacyjne, które uruchamiają się, gdy krok zawiedzie. Te kompensacje również muszą być idempotentne. Jeśli „refunduj płatność” uruchomi się dwukrotnie, możesz rozwiązać oryginalny błąd, tworząc nowy.
Moja zasada tutaj jest brutalna i prosta. Każda Aktywność, każdy handler komend i każda kompensacja, która dotyka świata zewnętrznego, powinna być naturalnie idempotentna lub posiadać prawdziwy klucz idempotentny do systemu downstream.
Jak testować idempotentność przed produkcją
Większość zespołów testuje happy paths, a następnie udaje zdziwionych, gdy dochodzi do ponawiania. To za mało. Dla zespołów Go Testing Concurrent Go Code with testing/synctest omawia, jak pisać szybkie, deterministyczne testy dla pętli ponawiania i zachowania limitu czasu kontekstu bez uśpienia przez sztuczne opóźnienia.
Powinieneś mieć automatyczne testy co najmniej dla tych przypadków:
- serwit zatwierdza modyfikację, ale odpowiedź nigdy nie dociera do klienta
- dwa identyczne żądania rywalizują z tym samym kluczem idempotentnym
- ten sam klucz jest ponownie użyty z innym payloadem
- konsument zatwierdza swoją pracę w bazie danych i ulega awarii przed ack
- webhook jest odtwarzany z tym samym identyfikatorem dostawy
- dispatcher outbox publikuje to samo zdarzenie więcej niż raz
- Aktywność workflow kończy zewnętrzne wywołanie i ulega awarii przed zgłoszeniem zakończenia
- rekord idempotentny wygasa i przychodzi prawdziwe późne ponowienie
AWS wyraźnie zaleca kompleksowe zestawy testów, które obejmują pomyślne żądania, nieudane żądania i duplikaty żądań. To porządne i absolutnie poprawne zalecenie.
Dodałbym jeszcze jeden drill awaryjny. Zweryfikuj, że odtworzona odpowiedź jest semantycznie równoważna z pierwszym wynikiem. AWS omawia późno docierające ponowienia i argumentuje za odpowiedziami, które zachowują pierwotne znaczenie, nawet po zmianie stanu wewnętrznego. To różnica między „nie nastąpił dodatkowy efekt uboczny” a „wywołujący nadal ma spójną umowę.”
Subiektywne zasady, które ratują prawdziwe systemy
Oto zasady, które egzekwowałbym w przeglądzie architektury.
Po pierwsze, klucze idempotentne należą do intencji biznesowej, a nie prób transportowych.
Po drugie, scope’uj każdy klucz według tenant’a i operacji. Globalne przestrzenie kluczy to sposób, w jaki niezwiązane żądania kolidują.
Po trzecie, utrwalaj decyzję deduplikacji atomowo z modyfikacją. Jeśli tak nie jest, projekt jest zły.
Po czwarte, odrzucaj ponowienia tego samego klucza z różnym payloadem. Stripe i AWS robią to z dobrych powodów.
Po piąte, przechowuj klucze przez pełny horyzont odtwarzania procesu biznesowego, a nie przez najkrótsze okno kolejki.
Po szóste, połącz producerów z outbox, a konsumentów z śledzeniem ID wiadomości. Jedna strona bez drugiej to połowa projektu.
Po siódme, propaguj tę samą identyfikację operacji downstream, gdy akcja biznesowa jest taka sama. AWS wyraźnie zaleca przekazywanie tokenu idempotentnego wzdłuż łańcucha przetwarzania.
Po ósme, nigdy nie zakładaj, że marketing exactly-once usuwa potrzebę idempotentnych efektów ubocznych.
Jeśli brzmi to rygorystycznie, dobrze. Idempotentność to miejsce, gdzie optymistyczna architektura spotyka się z rzeczywistością produkcyjną. Nie potrzebujesz złożoności wszędzie. Ale wszędzie tam, gdzie duplikaty efektów ubocznych mogłyby zaszkodzić pieniądzowi, stanowi lub zaufaniu, idempotentność powinna być elementem pierwszorzędowym umowy.
Te same zasady stosują się bezpośrednio do tła agentów AI. Agenci pollujący, którzy twierdzą zadania, emitują powiadomienia lub wyzwalają wywołania narzędzi, potrzebują kluczy deduplikacji i protokołów twierdzenia idempotentnego tak samo mocno, jak API płatności. Aby dowiedzieć się, jak działa wzorzec twierdzenia i deduplikacji wewnątrz produkcyjnych asystentów AI, zobacz Polling Agents in AI Assistants: 11 Implementation Patterns.