Idempotentność w systemach rozproszonych, która naprawdę działa
Unikaj powielania efektów ubocznych
Idempotentność w systemach rozproszonych to cecha, która ratuje sytuację, gdy sieć kłamie, kolejka ponawia wysyłkę, klient panikuje, a operator naciska przycisk odtworzenia. W systemach produkcyjnych wielokrotna dostawa jest normą. Wielokrotne skutki uboczne to błąd.
Protokół HTTP definiuje metodę idempotentną jako taką, w której wiele identycznych żądań ma taki sam zamierzony efekt na serwerze, co pojedyncze żądanie. Dlatego metody PUT, DELETE oraz 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 odpowiedzią na pytanie z trivia o HTTP. To gwarancja biznesowa. Jeśli klient naciśnie przycisk „zapłać” raz, nie możesz naliczyć opłaty dwa razy, ponieważ między zatwierdzeniem a odpowiedzią wystąpił limit czasu. Jeśli pracownik aktualizuje stan magazynowy i zawodzi przed potwierdzeniem wiadomości, nie możesz zmniejszyć stan magazynowy dwa razy, ponieważ broker ponownie dostarczył wiadomość. To jest standard.
Błąd, który widzę raz po raz, to traktowanie idempotentności jako funkcji transportowej, a nie właściwości systemu. Odradzanie duplikatów w kolejce, czasowniki HTTP i ponawianie żądań przez klienta pomagają, ale żaden z nich nie uratuje projektu, który pozwala, by ten sam zamiar biznesowy stworzył drugi skutek uboczny. Jeśli chcesz szerszego kontekstu dotyczącego tego, jak te decyzje integracyjne wpisują się w granice usług i kompromisy trwałości danych, zacznij od Architektura aplikacji w produkcji: wzorce integracyjne, projekt kodu i dostęp do danych.
Skąd pochodzą duplikaty w produkcji
Duplikaty nie pojawiają się z powodu nieuwagi zespołów. Pojawiają się, ponieważ systemy rozproszone ponawiają próby, zmieniają kolejność i odtwarzają dane.
Klient może wysłać żądanie utworzenia, serwer może je zatwierdzić, a odpowiedź może nadal zniknąć w trakcie transmisji. Dlatego HTTP rozróżnia metody idempotentne i dlatego interfejsy API płatności, takie jak Stripe i PayPal, oferują jawne mechanizmy idempotentności dla metod niebezpiecznych, takich jak POST.
Brokery wiadomości czynią ten problem jeszcze bardziej oczywistym. Dostawa „przynajmniej raz” oznacza, że konsument może być wywoływany wielokrotnie dla tej samej wiadomości, a obsłużnik może pomyślnie zaktualizować bazę danych, ale zawieść przed potwierdzeniem, co spowoduje, że broker dostarczy tę samą wiadomość ponownie.
Webhooki nie są niczym inne. GitHub twierdzi, że dostawy webhooków mogą przybywać w innej kolejności, nieudane dostawy nie są automatycznie ponawiane, a każda dostawa zawiera unikalny identyfikator GUID X-GitHub-Delivery, który należy stosować przy ochronie przed odtworzeniem. Dla praktycznego widoku architektury punktów końcowych czatu jako granic interakcji zobacz [Platformy czatu jako interfejsy systemów w nowoczesnych systemach](https://www.glukhov.org/pl/app-architecture/integration-patterns/chat-platforms-as-system-interfaces/ “Zobacz, jak Slack i Discord działają jako interfejsy systemowe dla przepływów powiadamiania i kontroli „człowiek w pętli” w nowoczesnych architekturach rozproszonych.").
Nawet systemy reklamujące silniejsze gwarancje pozostawiają Ci pracę do wykonania. Kafka może zapobiegać duplikatom wpisów w logach Kafka dzięki producentom idempotentnym i może zapewniać dostawę „dokładnie raz” dla przepływów odczyt-obróbk-zapis, które pozostają wewnątrz Kafki przy użyciu transakcji i konsumentów read_committed. Jednak własne dokumenty projektowe Kafki jasno wskazują, że zewnętrzne systemy nadal wymagają koordynacji z offsetami i danymi wyjściowymi. Dostawa „dokładnie raz” Google Cloud Pub/Sub jest ograniczona do subskrypcji pull, w obrębie regionu chmurowego i nadal wymaga od klientów śledzenia postępu przetwarzania do momentu udanego potwierdzenia.
Moje opiniotwórcze podsumowanie jest proste. Zakładaj, że transport będzie ponawiać próby. Zakładaj, że operatorzy będą odtwarzać dane. Zakładaj, że webhooki przybędą z opóźnieniem. Zaprojektuj ścieżkę zapisu tak, aby powtarzający się zamiar nie mógł stworzyć drugiego efektu biznesowego.
Kontrakt API, któremu naprawdę ufam
Jak klucze idempotentności zapobiegają duplikatom żądań API
Jedynym kontraktem API, któremu ufam w operacjach mutacyjnych, jest zamiar dostarczony przez wywołującego oraz trwałość po stronie serwera.
AWS zaleca identyfikator żądania dostarczony przez wywołującego i ostrzega, że usługa musi atomowo zarejestrować token idempotentności wraz z pracą mutacyjną. 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 ponownych prób. PayPal używa nagłówka PayPal-Request-Id w obsługiwanych interfejsach API POST i zwraca najnowszy status poprzedniego żądania z tym samym nagłówkiem.
Prowadzi to do praktycznego kontraktu:
- Klient generuje klucz idempotentności dla operacji biznesowej.
- Serwer zakreśla ten klucz według najemcy (tenant) i nazwy operacji.
- Serwer przechowuje hash żądania, aby ten sam klucz nie mógł zostać ponownie użyty dla innego ładunku.
- Serwer rejestruje stan, taki jak
pending(oczekujący),completed(ukończony) lubfailed(nieudany). - Ponowne próby z tym samym kluczem zwracają albo przechowywany wynik,要么 stabilny wskaźnik do niego.
- Ponowne próby z tym samym kluczem, ale innym ładunkiem kończą się jawną porażką.
Istnieje projekt nagłówka Idempotency-Key IETF, ale na dzień 2026-05-09 jest on nadal wymieniony w śledzeniu IETF Datatracker jako wygasły Internet-Draft, a nie opublikowana RFC. W praktyce nazwa nagłówka jest nadal szeroko użyteczna jako de facto konwencja, ale należy udokumentować kontrakt we własnym API, zamiast udawać, że standard jest ukończony.
Co powinien reprezentować klucz? Zamiar. Nie próbę HTTP. Nie połączenie TCP. Nie licznik ponownych prób. Jeśli użytkownik oznacza „utwórz zamówienie 123 raz”, każda ponowna próba dla tej samej komendy musi ponownie użyć tego samego klucza. Jeśli użytkownik oznacza „złóż drugie zamówienie”, musi użyć innego klucza.
Identyfikator żądania służy do śledzenia. Klucz idempotentności służy do poprawności. Jeśli pomyli się te dwie rzeczy, tablice pomiarowe będą wyglądać schludnie, podczas gdy Twoje pieniądze zostaną pobrane dwa razy.
Dlaczego PUT nie wystarczy
Nie, HTTP PUT nie wystarczy, aby czynić operację idempotentną.
Tak, RFC 9110 nadaje metodzie PUT semantykę idempotentną. Ale jeśli Twój obsłużnik PUT emituje nowe zdarzenie dół strumienia, wysyła e-mail przy każdej ponownej próbie lub ponownie nalicza zewnętrznego dostawcę, to Twoja implementacja naruszyła kontrakt biznesowy, nawet jeśli nazwa trasy wygląda godnie.
Wybór czasownika pomaga klientom zrozumieć zamiar. Nie implementuje zamiaru za Ciebie.
Używaj PUT, gdy model zasobu naprawdę pasuje do operacji pełnej wymiany lub typu upsert. Używaj POST, gdy tworzysz komendy lub akcje. Ale dla każdej mutacji, która może być ponawiana przez granice sieciowe, udokumentuj jawny kontrakt idempotentności. Jeśli Twoje akcje mutacyjne są uruchamiane z przepływów czatu, ten sam kontrakt stosuje się w Wzorce integracji Slacka dla powiadomień i przepływów pracy oraz [Wzorzec integracji Discorda dla powiadomień i pętli kontrolnych](https://www.glukhov.org/pl/app-architecture/integration-patterns/discord/ “Dogłębne omówienie webhooków i botów Discorda dla powiadomień, zatwierdzeń i kontroli „człowiek w pętli”. Przykłady w Go i Pythonie, bezpieczeństwo, idempotentność i routing."). Ukryte skutki uboczne to miejsce, gdzie architektura umiera.
Jak długo należy przechowywać klucz idempotentności
Dłużej, niż chce Twój zespół transportowy.
Stripe twierdzi, że klucze mogą być usuwane po upływie co najmniej 24 godzin. PayPal mówi, że okres retencji jest specyficzny dla API i podaje przykłady, które mogą trwać do 45 dni. Amazon SQS FIFO odradza duplikaty tylko w oknie 5-minutowym. GitHub przechowuje niedawne dostawy przez 3 dni w celu ręcznego ponownego przesyłania. Te liczby są dziko 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 transportowe do warstwy biznesowej.
Przechowuj rekordy idempotentności przez co najmniej maksymalne z tych okien:
- horyzont ponawiania żądań klienta
- horyzont ponownego przesyłania kolejki
- horyzont odtwarzania webhooków
- horyzont odtwarzania przez operatora
- horyzont rozliczenia lub rekompensaty dla operacji przenoszących pieniądze
W przypadku płatności, rezerwacji i provisioningu oznacza to często godziny lub dni, a nie minuty.
AWS wskazuje również dwa antywzorce, z którymi całkowicie się zgadzam. Nie używaj znaczników czasu jako klucza, ponieważ przesunięcie zegara i kolizje czynią je niezawodnymi. Nie przechowuj ślepej całości ładunku żądania jako rekordu odradzania duplikatów dla każdego żądania, ponieważ szkodli to wydajności i skalowalności. Przechowuj znormalizowany hash żądania plus minimalny stan odpowiedzi potrzebny do bezpiecznego odtworzenia. Jeśli musisz odtworzyć pierwszą odpowiedź bajt po bajcie, przechowuj kanoniczne ciało odpowiedzi w sposób, w jaki robi to Stripe.
Wzorce bazodanowe, które czynią idempotentność realną
Idempotentność staje się realna, gdy warstwa trwałości może wygrać wyścig dokładnie raz.
PostgreSQL daje Ci tu dwa krytyczne prymitywy. Ograniczenia unikalności egzekwują unikalność w jednej lub więcej kolumnach, a INSERT ... ON CONFLICT pozwala zdefiniować alternatywne działanie zamiast porażki przy naruszeniu unikalności. PostgreSQL dokumentuje również, że ON CONFLICT DO UPDATE gwarantuje atomowy wynik wstawienia lub aktualizacji przy jednoczesności.
Oznacza to, że 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żną częścią nie jest składnia. Ważną częścią jest atomowość. Rejestrowanie klucza i wykonywanie mutacji muszą się powieść lub zawieść razem. AWS mówi to jawnie dla idempotentności API, a ta sama reguła stosuje się w usługach wspieranych przez SQL.
Nie wykonuj naiwnej sekwencji „sprawdź, a potem działaj”, takiej jak „wybierz klucz; jeśli brakujący, to wstaw zamówienie”. Przy jednoczesności dwa żądania mogą przejść sprawdzenie i oba stworzą skutek uboczny. Ograniczenie unikalności nie jest opcjonalne. To mechanizm, który przekształca Twoją architekturę z optymistycznego mitu w coś, co można udowodnić pod obciążeniem.
Oto reguła, której używam w recenzjach. Jeśli decyzja odradzania duplikatów nie jest chroniona tą samą granicą transakcyjną co mutacja, nie masz idempotentności. Masz nadzieję.
Wiadomości, zdarzenia i webhooki potrzebują własnej granicy
Jak konsumenci obsługują duplikaty zdarzeń i wiadomości
Dla konsumentów wiadomości klasycznym wzorcem jest nadal ten sam. Zarejestruj przetworzone ID wiadomości w tej samej transakcji bazy danych co aktualizacja biznesowa. Chris Richardson opisuje podejście tabeli PROCESSED_MESSAGES bezpośrednio, używając klucza głównego dla subskrybenta i ID wiadomości, aby duplikaty kończyły się czysto i mogły być ignorowane.
Wiele zespołów nazywa ten jawnie sklep processed_messages tabelą skrzynki odbiorczej. Etykieta ma mniejsze znaczenie niż reguła. Odbiorca musi utrwalic dowód, że już obsłużył wiadomość, zanim ponowna próba może bezpiecznie nic nie zrobić.
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.
Jest też zwykle lepszy niż próba opierania się na marketingowych terminach brokera. Wsparcie Kafki dla „dokładnie raz” jest doskonałe, gdy pozostajesz w ramach własnego modelu transakcyjnego Kafki, ale dokumentacja Kafki nadal ostrzega, że zewnętrzne miejsca docelowe wymagają współpracy. SQS FIFO redukuje wysyłkę duplikatów tylko w swoim oknie odradzania duplikatów 5-minutowym. Pub/Sub „dokładnie raz” nadal oczekuje, że subskrybent będzie śledzić postęp i unikać duplikowania pracy, gdy potwierdzenia zawiodą.
„Dokładnie raz” jest zwykle optymalizacją lokalną. Idempotentne skutki uboczne to gwarancja systemu.
Połącz odradzanie duplikatów ze wzorcem outbox
Jeśli Twoja usługa aktualizuje stan lokalny i również publikuje zdarzenie, idempotentne konsumowanie samo w sobie nie wystarczy. Potrzebujesz również bezpiecznego sposobu, aby wysłać zdarzenie na zewnątrz po zatwierdzeniu lokalnej transakcji.
Dlatego wzorzec transakcyjnego outbox ma znaczenie. Chris Richardson opisuje podstawową ideę jako zapisywanie zdarzenia do tabeli outbox w tej samej transakcji co aktualizacja biznesowa, a następnie publikowanie go asynchronicznie. Debezium mówi, że wzorzec outbox unika niespójności między stanem wewnętrznym usługi a zdarzeniami konsumowanymi przez inne usługi. NServiceBus idzie dalej i pokazuje, jak przetwarzanie outbox odradza przychodzące wiadomości i unika rekordów zombie oraz wiadomości duchów.
Oto architektura, którą polecam dla usług, które posiadają dane i publikują zdarzenia integracyjne:
- Zwaliduj i utrwal komendę pod kluczem idempotentności.
- Napisz stan biznesowy i zdarzenie outbox w jednej lokalnej transakcji.
- Pozwól CDC lub dyspozytorowi outbox na publikację zdarzenia.
- Zrób również dół strumienia konsumentów idempotentnymi.
Outbox nie usuwa potrzeby idempotentnych konsumentów. Usuwa potrzebę udawania, że zatwierdzenie bazy danych i publikacja brokera mogą być jedną magiczną rozproszoną transakcją, gdy zwykle nie mogą.
Webhooki to tylko wiadomości z lepszym brandingiem
Traktuj przychodzące webhooki dokładnie jak wiadomości z niezaufanej krawędzi sieci.
GitHub dokumentuje, że dostawy mogą przybywać w innej kolejności, zaleca używanie X-Hub-Signature-256 do weryfikacji autentyczności i dostarcza X-GitHub-Delivery jako unikalny identyfikator dostawy. Zaznacza również, że ponowne dostawy ponownie używają tego samego ID dostawy.
Więc architektura jest prosta:
- zweryfikuj podpis jako pierwszy
- użyj GUID dostawy jako klucza odradzania duplikatów
- utrwal odbiór przed skutkami ubocznymi
- zrób obsłużniki świadomymi kolejności, zamiast zakładać kolejność przybycia *.enqueue ciężką pracę i zwróć szybką odpowiedź
Jeśli Twój obsłużnik webhooków zapisuje bezpośrednio do tabel biznesowych przed zarejestrowaniem odbioru, nie jest gotowy do produkcji. Jest tylko szybszy w robieniu błędów duplikatów.
Sagas i silniki przepływów pracy nadal potrzebują idempotentności
Sagas i trwałe silniki przepływów pracy nie usuwają problemu. Uczą go widoczny.
Temporal zaleca pisanie Activity tak, aby były idempotentne, ponieważ Activity mogą być ponawiane po awariach lub limitach czasu. Jego dokumentacja nawet wskazuje na przypadek brzegowy, w którym pracownik pomyślnie kończy zewnętrzny skutek uboczny, ale zawodzi przed zgłoszeniem ukończenia, co powoduje ponowne uruchomienie Activity. Temporal sugeruje również używanie kombinacji ID uruchomienia przepływu pracy i ID Activity jako stabilnego klucza idempotentności przy wywoływaniu usług dół strumienia. Jeśli stosujesz to w orkiestracji usług, Mikrousługi Go dla orkiestracji AI/ML omawia szersze kompromisy przepływów pracy.
To dokładnie właściwy model umysłowy. Silnik przepływu pracy może zachować historię wykonania i koordynować ponowne próby. Nie może retroaktywnie anulować obciążenia karty lub cofnąć wysłanego e-maila, chyba że Twoja aplikacja da mu idempotentne kroki i idempotentne rekompensaty.
To samo dotyczy sag. Własne wytyczne Temporal dla sag opisują akcje rekompensujące, które uruchamiają się, gdy krok zawodzi. Te rekompensaty muszą również być idempotentne. Jeśli „zwrot płatności” uruchomi się dwa razy, możesz rozwiązać oryginalny błąd, tworząc nowy.
Moja reguła tutaj jest brutalna i prosta. Każde Activity, każdy obsłużnik komend i każda rekompensata, która dotyka świata zewnętrznego, powinna być albo naturalnie idempotentna,要么 nosić prawdziwy klucz idempotentności do systemu dół strumienia.
Jak testować idempotentność przed produkcją
Większość zespołów testuje szczęśliwe ścieżki, a potem dziwi się, gdy występują ponowne próby. To nie wystarczy.
Powinieneś mieć zautomatyzowane testy co najmniej dla tych przypadków:
- serwer zatwierdza mutację, ale odpowiedź nigdy nie dociera do klienta
- dwa identyczne żądania wyścigują z tym samym kluczem idempotentności
- ten sam klucz jest ponownie używany z innym ładunkiem
- konsument zatwierdza pracę bazy danych i zawodzi przed ack
- webhook jest odtwarzany z tym samym ID dostawy
- dyspozytor outbox publikuje to samo zdarzenie więcej niż raz
- Activity przepływu pracy kończy zewnętrzne wywołanie i zawodzi przed zgłoszeniem ukończenia
- rekord idempotentności wygasa i przybywa prawdziwa późna ponowna próba
AWS jawnie zaleca kompleksowe zestawy testowe, które obejmują udane żądania, nieudane żądania i żądania duplikatów. Ta rada jest pospolita i absolutnie poprawna.
Dodam jedną dodatkową próbę awarii. Zweryfikuj, że odtworzona odpowiedź jest semantycznie równoważna z pierwszym wynikiem. AWS omawia późno przybywające ponowne próby i argumentuje za odpowiedziami, które zachowują oryginalne znaczenie, nawet po zmianie stanu podstawowego. To jest różnica między „nie nastąpił dodatkowy skutek uboczny” a „wywołujący nadal ma spójny kontrakt”.
Opiniotwórcze reguły, które ratują prawdziwe systemy
Oto reguły, które wprowadziłbym w recenzji architektury.
Po pierwsze, klucze idempotentności należą do zamiaru biznesowego, a nie do prób transportowych.
Po drugie, zakreśl każdy klucz według najemcy i operacji. Globalne przestrzenie kluczy to sposób, w jaki niepowiążane żądania kolizują.
Po trzecie, utrwal decyzję odradzania duplikatów atomowo z mutacją. Jeśli to nie jest prawdą, projekt jest błędny.
Po czwarte, odrzucaj ponowne próby z tym samym kluczem i innym ładunkiem. Stripe i AWS robią to z dobrym powodem.
Po piąte, przechowuj klucze przez cały horyzont odtwarzania procesu biznesowego, a nie przez najkrótsze okno kolejki.
Po szóste, łącz producentów z outbox i konsumentów ze śledzeniem ID wiadomości. Jedna strona bez drugiej to połowa projektu.
Po siódme, propaguj tę samą tożsamość operacji dół strumienia, gdy akcja biznesowa jest taka sama. AWS jawnie zaleca przekazywanie tokena idempotentności wzdłuż łańcucha przetwarzania.
Po ósme, nigdy nie zakładaj, że marketing „dokładnie raz” usuwa potrzebę idempotentnych skutków ubocznych.
Jeśli to brzmi rygorystycznie, to dobrze. Idempotentność to miejsce, gdzie optymistyczna architektura spotyka się z rzeczywistością produkcyjną. Nie potrzebujesz złożoności wszędzie. Ale tam, gdzie duplikaty skutków ubocznych zaszkodzą pieniądzom, stanowi lub zaufaniu, idempotentność powinna być pierwszoklasową częścią kontraktu.