Wzór skrzynki zewnętrznej
Outbox jest wzorcem architektonicznym, w którym usługa domeny zapisuje zmianę firmy i odpowiadające jej zdarzenie w jednej transakcji lokalnej do repozytorium. Publikacja zdarzenia do zewnętrznej magistrali/kolejki odbywa się asynchronicznie za pomocą oddzielnego bezpiecznego procesu (wydawcy), który czyta tabelę 'outbox' i przekazuje rekordy. To podejście eliminuje wyścig „najpierw do bazy danych, potem do autobusu” i zapewnia niezawodną dostawę nawet w przypadku awarii.
1) Kiedy stosować
Pasuje do:- Mikroservice i modułowe monolity z wydarzeniami między kontekstami.
- Wymagane jest, aby zapewnić, że „stan jest ustalony” zdarzenie nie może zostać utracone.
- Potrzebujemy idempotencji i kontrolowanej ponownej dostawy.
- Trudne globalne transakcje na kilku zasobach mają kluczowe znaczenie (lepsze niż TCC/sagi z wyraźnymi umowami).
- Nie ma dedykowanego źródła prawdy (stan nie jest przechowywany tam, gdzie zdarzenie jest generowane).
2) Cele i właściwości
Zapisz atomowy: rekord domeny + skrzynka zewnętrzna - w jednej transakcji.
Publikacja przynajmniej raz: zezwalamy na powtarzanie, wykluczamy straty.
Idempotencja konsumentów: ochrona przed przejmowaniem strony abonenta.
Efektywny dokładnie raz: osiągnięty przez połączenie outbox + idempotent consumer + dedup.
Clear telemetry - Koreluj transakcje i wydarzenia biznesowe.
3) Schemat danych (przykład)
sql
-- Domain table (example: orders)
CREATE TABLE orders (
id UUID PRIMARY KEY,
tenant_id TEXT NOT NULL,
status TEXT NOT NULL,
total_amount NUMERIC(12,2) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT now()
);
-- Outbox
CREATE TABLE outbox (
id UUID PRIMARY KEY, -- event_id aggregate_type TEXT NOT NULL, -- 'order'
aggregate_id UUID NOT NULL, -- order_id tenant_id TEXT NOT NULL,
type TEXT NOT NULL, -- 'OrderCreated'
payload JSONB NOT NULL, -- serialized headers event JSONB NOT NULL DEFAULT '{}':: jsonb,
occurred_at TIMESTAMP NOT NULL, -- time in domain transaction available_at TIMESTAMP NOT NULL, -- earliest publish time (backoff)
published_at TIMESTAMP, - is filled by the attempts INT NOT NULL DEFAULT 0,
error TEXT
);
CREATE INDEX ON outbox (available_at) WHERE published_at IS NULL;
CREATE INDEX ON outbox (tenant_id, available_at) WHERE published_at IS NULL;
4) Warstwa aplikacji
pseudo begin tx domainChange () # INSERT/UPDATE in domain table insert into outbox (event) # event with aggregate/tenant commit tx keys
Jeśli commit się powiedzie, zdarzenie w skrzynce jest gwarantowane. Jeśli aplikacja spadnie po zatwierdzeniu, wydawca nadrobi zaległości.
5) Wydawca (czytelnik → wydawca)
Zadania:- Okresowo odczytywane niepublikowane zdarzenia ('published _ at IS NULL' i' available _ at <= now () '), partie.
- Spróbuj opublikować do magistrali/kolejki; jeśli się powiedzie, zaznacz „published _ at”.
- W przypadku błędu - zwiększyć 'próby', umieścić 'available _ at' na przyszłość (wykładnicze backoff), napisz 'błąd'.
- Przestrzegaj ograniczeń dotyczących najemców/kluczy (uczciwość), nie blokuj produktu.
pseudo loop:
events = select from outbox where published_at is null and available_at <= now()
order by occurred_at limit BATCH_SIZE for update skip locked
for e in events:
try:
broker. publish(topicFor(e), serialize(e. payload), headers(e))
markPublished(e. id, now())
except Retryable:
backoff = computeBackoff(e. attempts)
reschedule(e. id, now()+backoff, attempts+1, last_error)
except NonRetryable:
moveToDLQ (e) or markError (e) # by sleep (POLL_INTERVAL) policy
6) Idempotencja i deduplikacja
Po stronie konsumenta (skrzynka odbiorcza/sklep Idempotency):sql
CREATE TABLE inbox (
consumer_name TEXT,
event_id UUID,
processed_at TIMESTAMP NOT NULL,
PRIMARY KEY (consumer_name, event_id)
);
Algorytm: podczas otrzymywania zdarzenia najpierw spróbuj 'INSERT' w 'skrzynce odbiorczej'; jeśli istnieje kluczowy konflikt, wydarzenie zostało już załatwione → no-op. Następna jest logika biznesu.
Po stronie wydawcy: 'Idempotence-Key' w nagłówkach (na przykład 'event _ id'), aby autobus/broker/proxy mógł filtrować duplikaty.
7) Porządek i przyczynowość
Lokalne zamówienie przez 'agregate _ id' jest dostarczane przez sortowanie' occured _ at'i publikowanie „według klucza”.
W przypadku autobusów logarytmicznych z partycją - partycja z kluczem 'agregate _ id'/' lokator _ id', tak aby zdarzenia jednego agregatu były w tym samym przegrodzeniu.
Jeśli zamówienie jest krytyczne, należy unikać wyścigów wydawców jednokluczykowych.
8) CDC (zmiana przechwytywania danych)
Zamiast aktywnego wydawcy można użyć CDC: silnik odczytuje dziennik transakcji bazy danych i tłumaczy linie „outbox” na bus.Pls - minimalne obciążenie bazy danych, dokładna sekwencja, brak sondażu. Wady - komplikacje działania i powiązanie ze specyfiką DBMS. Oba podejścia są ważne; wybrać według kompetencji i SLO.
9) Błędy, DLQ i Redrive
Retryyable (network, limits) - zwiększenie 'attempts', odroczenie 'available _ at' (wykładnicze backoff + jitter).
Non-retryyable (invalid scheme/contract) - przeniesiony do DLQ/Dead-Letter Topic z bogatymi metadanymi.
Bezpieczne Redrive: partie, limit stawki, walidacja programu, priorytet poniżej ruchu produkcyjnego.
10) Wielozatrudnienie i limity
Wymagane znaczniki: 'lokator _ id',' plan ',' region '- w' outbox '. nagłówki ".
Uczciwość dla jednego najemcy: wydawca rozpowszechnia „okna” publikacji i ograniczenia prób dla najemców.
Miejsce zamieszkania: przechowywać skrzynkę zewnętrzną w tym samym regionie co dane domeny; publikacja międzyregionalna - tylko agregaty/podsumowania.
11) Bezpieczeństwo i zgodność
Wydanie PII w ładunku/nagłówkach dotyczących polityki lokatora/regionu.
Podpis/szyfrowanie ładunku, jeśli autobus jest obcy.
Audyt wszystkich przejść państwowych: utworzone, opublikowane, błędne, przeredagowane.
12) Obserwowalność
Metryka:- Opóźnienie publikacji ("teraz - occurred_at' p50/p95/p99).
- Wskaźnik sukcesu, wskaźnik błędów, przyczyna dystrybucji.
- Rozmiar skrzynki zewnętrznej (liczba niepublikowanych), powtórzenia/s
- Na najemcę wykresy przepustowości i opóźnienia.
- Korelacja 'event _ id'/' aggregate _ id'/' saga _ id'; przęsła „db-tx”, „publish”, „retry”.
- Adnotacje: 'próba', 'backoff _ ms', 'dlq = true'.
- Krótkie rekordy sukcesu; pełne szczegóły na błąd/przeredagowanie.
13) Testowanie i chaos
Test atomowości: sztucznie „upadek” po dokonaniu transakcji domeny przed publikacją - wydarzenie musi zostać zwolnione później.
Duplikat testu: publikujemy to samo wydarzenie kilka razy - konsument wykonuje dokładnie jeden efekt (skrzynka odbiorcza).
Próba zamówienia: partia zdarzeń według jednego kruszywa - kontrola sekwencji/idempotencji.
Chaos: niepowodzenie maklerskie, wzrost opóźnień w bazie danych, głosiciele podzielonych mózgów, zegar-skew.
14) Szablony konfiguracji (przykład)
yaml outbox:
poll_interval_ms: 200 batch_size: 200 order_by: occurred_at backoff:
strategy: exponential_full_jitter initial_ms: 250 max_ms: 10_000 max_attempts: 20 fairness:
per_tenant_parallelism: 4 per_key_serial: true
publisher:
rate_limit_per_sec: 500 headers:
idempotency_key: event_id schema_version: v3 dlq:
enabled: true topic: myapp. events. dlq include_metadata:
- error
- attempts
- source_table
- tenant_id
- aggregate_id
15) Integracja z sagami i rekolekcjami
Outbox - „transport bezpieczeństwa” dla etapów sagi: lokalna transakcja zapisuje efekt i polecenie/zdarzenie; publikacja - niezawodne i dozowane.
Zasady powtarzania i cofania muszą być zgodne z „Retry-After” i „Circuit Breaker”; unikać „burzy retray”.
16) Typowe błędy
Piszą zdarzenie po popełnieniu stanu domeny - strata podczas upadku jest możliwa.
Brak indeksów/archiwum w 'outbox' → wzrost latencji publikacji.
Wydawca bez „SKIP LOCKED” lub bez ostrzału - konkurencja i blokowanie.
Brak idempotencji wśród konsumentów - duplikaty i skutki uboczne.
Mieszanie PII bez maskowania w DLQ/logi.
Jedna globalna kolejka wydawnicza bez uczciwości - „hałaśliwy” lokator spowalnia wszystkich.
Brak monitorowania opóźnień → utajona degradacja.
17) Szybki wybór strategii
Poziom początkowy: sondaż z bazy danych, 100-500 partii, pełny jitter backoff, skrzynka odbiorcza dla konsumentów.
Wysokie obciążenie: CDC z dziennika transakcji, shading przez 'lokator _ id/aggregate _ id', WFQ przez najemcę.
Ścisłe zamówienie według kruszywa: publikacja seryjna na klucz (mutex), podział tematu z kluczem.
Zgodność/PII: szyfrowanie ładunku, wydanie DLQ, regionalna skrzynka odbiorcza.
18) Lista kontrolna przedsprzedaży
- Zmiany domeny i zapisy do 'outbox' występują w tej samej transakcji.
- Wydawca obsługuje partie, używa „SKIP LOCKED”, backoff z jitterem i limitami.
- Konsumenci są idempotentni (tablica „skrzynka odbiorcza ”/dziennik deadup).
- Konfiguracja DLQ i Secure Release.
- Wskaźniki lag/error i alert na progach p95/p99.
- Kluczowe zamówienie jest gwarantowane (partie/serie).
- Archiwum/retencja 'outbox' i jasne opublikowane zapisy.
- Polityka PII i audyt państwa w okresie przejściowym.
- Upuść testy między commit i opublikować, duplikaty i zamówienie.
- Dokumentacja kontraktowa (schematy/wersje/kompatybilność).
Wnioski
Wzór skrzynki zewnętrznej zmienia „kruchy” pakiet „DB” w niezawodny rurociąg: utrwalenie stanu atomowego, gwarantowana (choć „przynajmniej raz”) publikacja, subskrybenci idempotent i kontrolowane redrave. Dzięki odpowiedniej telemetrii, limitom i dyscyplinie schematu daje praktyczne zachowanie dokładnie raz, zmniejszając złożoność transakcji rozproszonych i zwiększając odporność systemu na awarie i obciążenia szczytowe.