Transaktionel publicering af hændelser og outbox
Garanter levering af hændelser med event publication registry og det transaktionelle outbox-mønster.
Transaktionel publicering af hændelser og outbox er en gratis Komplet guide til Spring Boot 4-lektion på CoddyKit. Dette er lektion 3 af 4. Du kan læse alle 3 lektioner i dette læringsspor gratis i deres fulde længde — derefter låser CoddyKit PRO alle lektioner op samt praktiske øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Den er en del af læringsforløbet i Komplet guide til Spring Boot 4, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Komplet guide til Spring Boot 4-kurset indeholder 4 lektioner i alt.
Hvorfor hændelser går tabt
I en hændelsesdrevet Spring Modulith-applikation kommunikerer moduler med hinanden ved at publicere applikationshændelser. Et modul opretter en hændelse i en transaktion, og lyttere i andre moduler reagerer på den.
Faren er, at når du som standard publicerer en hændelse, og en lytter behandler den asynkront (eller i en separat transaktion), kan lytterens arbejde mislykkes, efter at udgiveren allerede har committet. Resultatet er en hændelse, der går tabt — udgiverens tilstand blev ændret, men den efterfølgende sideeffekt fandt aldrig sted.
- Udgiveren committer en
Ordersom PAID. - Den asynkrone lytter, der sender en bekræftelsesmail, går ned.
- Ingen prøver igen — kunden modtager aldrig mailen.
Denne lektion viser, hvordan Spring Moduliths Event Publication Registry implementerer mønsteret med en transaktionel outbox for at garantere levering.
Mønsteret med en transaktionel outbox
Mønsteret med en transaktionel outbox løser problemet med to skrivninger: Du skal atomisk (1) ændre din forretningstilstand og (2) registrere, at en hændelse skal leveres.
I stedet for at forsøge at skrive til databasen og en meddelelsesmægler i én transaktion (umuligt uden distribuerede transaktioner), skriver du begge dele til den samme database i den samme lokale transaktion:
- Forretningsændringen (f.eks. ordrerækken).
- En række i en outbox-tabel, der beskriver hændelsen.
En separat proces læser derefter ubehandlede outbox-rækker og leverer dem, mens hver række markeres som fuldført. Fordi outbox-skrivningen deler forretningstransaktionen, registreres en hændelse hvis og kun hvis forretningsændringen blev committed.
Spring Moduliths Event Publication Registry
Spring Modulith leveres med en færdig outbox: Event Publication Registry. Når en hændelse har en transaktionel lytter (annoteret med @ApplicationModuleListener), gør Modulith automatisk følgende:
- Skriver en række for hændelsespubliceringen, før lytteren kører.
- Markerer den som fuldført, når lytteren afsluttes uden fejl.
- Lader den stå som ufuldstændig, hvis lytteren kaster en fejl — så den kan behandles igen.
Du aktiverer den ved at tilføje starteren og et persistensmodul. Registret gemmer publiceringerne i en tabel som event_publication.
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-jpa</artifactId>
</dependency>@ApplicationModuleListener
Den vigtigste annotation er @ApplicationModuleListener. Det er en sammensat annotation, der kombinerer tre funktioner:
@Async— lytteren kører på en separat tråd, så modulerne afkobles.@Transactional(propagation = REQUIRES_NEW)— lytteren kører i sin egen transaktion.@TransactionalEventListener(phase = AFTER_COMMIT)— den udløses kun, efter at udgiverens transaktion er committed.
Sammen med registret på classpathen får hver hændelse, som håndteres af en sådan lytter, en outbox-post. Hvis lytteren mislykkes, forbliver publiceringen ufuldstændig og overlever genstarter.
@Component
class OrderNotifications {
@ApplicationModuleListener
void on(OrderCompleted event) {
// runs async, in a NEW transaction, after the publisher committed
emailService.sendConfirmation(event.orderId());
}
}Publicering af hændelsen
Publiceringssiden forbliver enkel. I en normal @Transactional-metode i en service kalder du ApplicationEventPublisher.publishEvent(...). Spring Modulith opfanger publiceringen og skriver, fordi der findes en transaktionel modullytter, outbox-rækken i den samme transaktion som din forretningsændring.
Brug uforanderlige Java-record-typer til hændelser — de er korte, serialiserbare og har en tydelig værdisemantik.
public record OrderCompleted(String orderId) {}
@Service
class OrderService {
private final OrderRepository orders;
private final ApplicationEventPublisher events;
OrderService(OrderRepository orders, ApplicationEventPublisher events) {
this.orders = orders;
this.events = events;
}
@Transactional
public void complete(String orderId) {
Order order = orders.findById(orderId).orElseThrow();
order.markCompleted(); // business change
events.publishEvent(new OrderCompleted(orderId)); // outbox row, same tx
}
}Sådan fungerer atomiciteten
Her er den afgørende sekvens, der garanterer levering:
- Din
complete()-metode åbner en transaktion og ændrer ordren. publishEventfår Modulith til at INDSÆTTE en ufuldstændig event_publication-række i den samme transaktion.- Transaktionen committer — ordreændringen og outbox-rækken gemmes sammen atomisk.
- Efter commit kører
@ApplicationModuleListeneri en ny transaktion. - Ved succes markeres publiceringen som fuldført.
Hvis JVM'en går ned mellem commit og en vellykket kørsel af lytteren, er rækken stadig ufuldstændig i databasen og klar til at blive publiceret igen. Ingen hændelse går nogensinde ubemærket tabt.
Tabellen event_publication
JPA-persistensmodulet gemmer publiceringer i en tabel. Hver række identificerer en serialiseret hændelse og dens mållytter samt tidsstempler. Kendskab til skemaet hjælper dig med at forstå genforsøg og overvågning.
id— primær UUID-nøgle.listener_id— fuldt kvalificeret lyttermethode, der skal håndtere den.event_type+serialized_event— hændelsens nyttedata (JSON som standard via Jackson).publication_date— hvornår den blev oprettet.completion_date— NULL, mens den er ufuldstændig; sættes, når lytteren lykkes.
En række med completion_date IS NULL er en udestående hændelse, der afventer (gen)levering.
CREATE TABLE event_publication (
id UUID NOT NULL,
listener_id TEXT NOT NULL,
event_type TEXT NOT NULL,
serialized_event TEXT NOT NULL,
publication_date TIMESTAMP WITH TIME ZONE NOT NULL,
completion_date TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (id)
);Genpublicering ved opstart
Ufuldstændige publiceringer er nytteløse, medmindre noget prøver igen. Spring Modulith kan genpublicere udestående hændelser ved opstart af applikationen, så den kan komme sig efter nedbrud, der skete midt under leveringen.
Aktivér det i application.properties:
spring.modulith.republish-outstanding-events-on-restart=true
Ved opstart læser Modulith alle ufuldstændige event_publication-rækker og kalder deres lyttere igen. Fordi lyttere bør være idempotente, er det sikkert at afspille en delvist behandlet hændelse igen.
# application.properties
spring.modulith.republish-outstanding-events-on-restart=true
# optional: also serialize events as JSON columns you can query
spring.modulith.events.jdbc.schema-initialization.enabled=trueIdempotente lyttere
Fordi en hændelse kan blive leveret mere end én gang (ved genforsøg efter et nedbrud eller planlagt genindsendelse), kræver garantien om levering mindst én gang, at dine lyttere er idempotente. Behandling af den samme hændelse to gange skal give samme sluttilstand som behandling én gang.
Almindelige teknikker:
- Brug en naturlig forretningsnøgle (ordre-id'et), og kontrollér, om sideeffekten allerede har fundet sted.
- Registrér behandlede hændelses-id'er i en dedupliseringstabel med en unik begrænsning.
- Gør handlingen naturligt idempotent (UPSERT, eller sæt en tilstand i stedet for at inkrementere).
@Component
class InventoryAdjuster {
private final ProcessedEventRepository processed;
@ApplicationModuleListener
void on(OrderCompleted event) {
// skip if we've already handled this exact event
if (!processed.markIfNew(event.orderId())) {
return;
}
inventory.release(event.orderId());
}
}Planlagt genindsendelse af ufuldstændige hændelser
Genpublicering ved opstart hjælper kun, når du genstarter. For tjenester, der kører længe, ønsker du også periodisk gendannelse af fastlåste publiceringer (f.eks. en lytter, der kastede en midlertidig fejl). Modulith tilbyder en planlægger til fuldførelse/genindsendelse.
spring.modulith.events.completion-mode— styrer, om fuldførte rækker slettes, arkiveres eller opdateres.- Aktivér en tilbagevendende genindsendelse, så ufuldstændige hændelser, der er ældre end en tærskel, automatisk behandles igen.
Sammen med idempotens gør dette outboxen til en selvhelende leveringskanal uden en separat meddelelsesmægler.
# application.properties
spring.modulith.events.republish-outstanding-events-on-restart=true
spring.modulith.events.completion-mode=update
# resubmit publications still incomplete after this interval
spring.modulith.events.republish-outstanding-events.interval=PT10MFra outbox til ekstern mægler
Det samme register kan forbinde til ekstern meddelelsesudveksling. Spring Modulith leverer eksternaliseringsmoduler til (Kafka, RabbitMQ, AMQP, SQS osv.). Du annoterer en hændelse med @Externalized, hvorefter en transaktionel lytter publicerer den til mæglere — understøttet af den selvsamme outbox.
Det betyder, at garantien om levering mindst én gang også gælder på tværs af procesgrænser: Afsendelsen til mægleren er selv en hændelsespublicering, der først markeres som fuldført, når meddelelsen er accepteret af mæglere.
import org.springframework.modulith.events.Externalized;
@Externalized("orders.completed::#{orderId()}")
public record OrderCompleted(String orderId) {}
// add: spring-modulith-events-kafka
// spring.modulith routes the event to topic "orders.completed"
// keyed by orderId, only after the publishing tx commitsHurtigt tjek
Test din forståelse af garantien ved en transaktionel outbox.
Opsummering
Du har lært, hvordan du garanterer hændelseslevering i Spring Modulith ved hjælp af mønstret med en transaktionel outbox:
- Registreringsdatabasen for hændelsespublicering gemmer en
event_publication-række i samme transaktion som din forretningsændring — dermed løses problemet med to skrivninger. @ApplicationModuleListener= asynkron + REQUIRES_NEW-transaktion + AFTER_COMMIT, så lyttere kører, efter publiceringen er committet, og får deres egen outbox-post.- En publicering er ufuldstændig, indtil dens lytter lykkes (
completion_date IS NULL); fejl betyder, at den bliver liggende til genforsøg. - Genpublicering ved genstart og planlagt genindsendelse gendanner fastlåste hændelser — dermed opnås levering mindst én gang.
- Da levering sker mindst én gang, skal lyttere være idempotente.
@Externalizedforbinder den samme outbox med Kafka/RabbitMQ/SQS, så der opnås garantier på tværs af processer.
Lær Java med en AI-underviser — gratis
Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.
- Kurser
- 21
- Lektioner
- 84
Ofte stillede spørgsmål
Er lektionen “Transaktionel publicering af hændelser og outbox” gratis?
Ja — alle 3 lektioner i læringssporet Komplet guide til Spring Boot 4, inklusive “Transaktionel publicering af hændelser og outbox”, kan læses gratis i deres fulde længde her på webstedet. Derefter låser CoddyKit PRO alle lektioner op samt interaktive øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Komplet guide til Spring Boot 4-kurset indeholder 4 lektioner i alt.
Hvad lærer jeg i “Transaktionel publicering af hændelser og outbox”?
Garanter levering af hændelser med event publication registry og det transaktionelle outbox-mønster. Du øver dig i Komplet guide til Spring Boot 4 med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.
Skal jeg have erfaring for at begynde på Komplet guide til Spring Boot 4?
Der kræves ingen tidligere erfaring. Komplet guide til Spring Boot 4 på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 3 af 4.
Hvor lang tid tager lektionen “Transaktionel publicering af hændelser og outbox”?
De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.
Kan jeg skrive og køre kode i denne Komplet guide til Spring Boot 4-lektion?
Ja. Alle Komplet guide til Spring Boot 4-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.
Alle lektioner i dette kursus
- Applikationsmoduler og verifikation af grænser
- Interne applikationshændelser og listeners
- Transaktionel publicering af hændelser og outbox
- Integrationstest af moduler og scenarier