Be Software Architecture Wise
Be Wise Apps

Read in English · Leer en español · Lire en français

Abschnitt 6.7 von 6.8 · Fortgeschritten · 30 Min. Lesezeit

Verteilte Transaktionen, Sagas und das Outbox-Muster

Videolektion

5 Min. · auf Englisch gesprochen, mit Untertiteln
Nur anhören (Audio)
Transkript lesen

Wenn eine Geschäftsaktion mehrere Services umfasst, kann keine einzelne Transaktion sie abdecken. In dieser Lektion erfahren Sie, warum man Two-Phase Commit zwischen Services meidet, wie Sagas Prozesse stattdessen korrekt halten und wie man Nachrichten zuverlässig sendet.

Einen Urlaub zu buchen heißt, einen Flug, ein Hotel und einen Mietwagen zu buchen, bei drei verschiedenen Unternehmen. Niemand kann alle drei im exakt selben Augenblick zustande bringen. Also buchen Sie eines nach dem anderen. Ist der Mietwagen dann nicht verfügbar, stornieren Sie Hotel und Flug, vielleicht gegen eine Stornogebühr. Das ist eine Saga: getrennte Schritte, jeder mit einem geplanten Weg, ihn rückgängig zu machen. Die Kunst ist, die Reihenfolge der Schritte zu wählen und die Stornierungen zu planen, bevor etwas schiefgeht.

Der klassische Weg, Änderungen in mehreren Datenbanken nach dem Alles-oder-nichts-Prinzip vorzunehmen, ist Two-Phase Commit, gesteuert von einem Koordinator. In Phase eins fragt er jeden Teilnehmer, ob er committen kann. Jeder speichert seine Änderungen dauerhaft, behält seine Sperren und stimmt mit Ja. Mit seinem Ja gibt er das Recht auf, eigenständig abzubrechen. Lauten alle Stimmen Ja, protokolliert der Koordinator seine Entscheidung: Commit. In Phase zwei teilt er sie jedem Teilnehmer mit, und diese wenden das Ergebnis an und geben ihre Sperren frei. Stürzt der Koordinator aber nach den Stimmen ab, können die Teilnehmer nicht allein entscheiden und behalten ihre Sperren, bis er sich erholt. Zwischen unabhängig verantworteten Services passt das schlecht. Die Verfügbarkeit multipliziert sich nach unten, Sperren werden über das Netzwerk gehalten, Deployments werden gekoppelt, und viele Teilnehmer, etwa ein Zahlungsanbieter, können gar nicht mitmachen.

Die Alternative ist eine Saga: eine Folge lokaler Transaktionen, jede in der eigenen Datenbank eines Service, verbunden durch Nachrichten. Jedem Schritt steht eine kompensierende Transaktion gegenüber, die ihn im fachlichen Sinn rückgängig macht. Entweder werden alle Schritte abgeschlossen, oder die abgeschlossenen werden in umgekehrter Reihenfolge kompensiert. Was eine Saga nicht bietet, ist Isolation: Zwischen den Schritten können andere den Zwischenzustand sehen.

Hier ist die Bestell-Saga der Essenslieferplattform, gesteuert vom Bestellservice als Orchestrator. Zuerst erfasst er die Bestellung als ausstehend, was anderen zeigt, dass die Bestellung im Fluss ist. Zweitens bittet er den Zahlungsservice, die Zahlung zu autorisieren: eine günstige Vormerkung auf der Karte, leicht zu stornieren. Drittens bittet er die Disposition, einen Kurier zu reservieren, den Schritt, der zu Stoßzeiten am ehesten scheitert. Diesmal ist kein Kurier verfügbar. Also kompensiert der Orchestrator die abgeschlossenen Schritte, die neuesten zuerst. Er storniert die Zahlungsautorisierung. Dann markiert er die Bestellung als abgelehnt, behält die Zeile für die Prüfspur und informiert den Kunden, bevor etwas gekocht oder abgebucht wurde. Wäre ein Kurier gefunden worden, wäre Schritt vier, die Bestätigung an das Restaurant, der Pivot. Sobald eine Küche zu kochen beginnt, kann die Saga nur noch vorwärts, daher werden spätere Schritte wiederholt, bis sie gelingen.

Ein Rollback macht es so, als wäre nichts geschehen. Eine Kompensation ist eine neue Transaktion, die etwas entgegenwirkt, das tatsächlich geschehen ist und vielleicht gesehen wurde. Sie kann die Vormerkung auf der Karte stornieren, aber keine E-Mail zurückholen. Kompensationen müssen daher idempotent sein, weil ihr Auslöser zweimal eintreffen kann, und wiederholbar, bis sie gelingen, oder einen Alarm für einen Menschen auslösen. Sie können sogar vor dem Schritt eintreffen, den sie rückgängig machen. Der Zahlungsservice sollte sich dann die Stornierung merken und die verspätete Autorisierung ablehnen. Und Schritte, die sich nicht rückgängig machen lassen, etwa das Kochen des Essens, gehören hinter den Pivot oder brauchen eine fachliche Kompensation wie einen Gutschein oder eine Erstattung.

Jeder Saga-Schritt aktualisiert seine Datenbank und sendet eine Nachricht. Beides getrennt zu tun, ist das Dual-Write-Problem: Ein Absturz dazwischen, und die Nachricht geht verloren oder verkündet etwas, das nie geschehen ist. Die Transactional Outbox löst das mit einer lokalen Transaktion. Der Bestellservice schreibt die Bestellzeile und die ausgehende Nachricht, als Zeile in einer Outbox-Tabelle, gemeinsam. Beide committen, oder keine. Ein separates Relay liest die Outbox, per Polling oder über das Änderungslog der Datenbank, und veröffentlicht jede Nachricht beim Broker. Ein Relay kann abstürzen, bevor es vermerkt, was es gesendet hat, also kann eine Nachricht zweimal hinausgehen. Deshalb führt der Empfänger eine Inbox verarbeiteter Nachrichten-IDs, gespeichert mit seiner Änderung, und überspringt Duplikate.

Wie wählen Sie also? Bevorzugen Sie, wo es geht, eine lokale Transaktion. Schneidet eine Serviceaufteilung durch eine Konsistenzgrenze, ist es oft günstiger, die Grenze zu verschieben. Setzen Sie eine Saga ein, wenn sich die Schritte kompensieren oder wiederholen lassen. Choreografie, bei der Services einfach auf die Ereignisse der anderen reagieren, passt für zwei oder drei Schritte. Für längere Abläufe mit Verzweigungen und Timeouts bevorzugen die meisten Fachleute einen Orchestrator. Und ordnen Sie die Schritte bewusst: wahrscheinliche Fehlschläge vor den Pivot, alles Unumkehrbare dahinter.

Der vollständige Abschnitt enthält die komplette Schritttabelle und die Gegenmaßnahmen gegen die fehlende Isolation von Sagas. Als Nächstes, in Abschnitt sechs Punkt acht, geht es um Serverless und Space-Based Architecture. Bis gleich.

Was Sie wissen müssen

Das Problem: eine Geschäftsaktion, mehrere Datenbanken

Innerhalb einer Datenbank macht eine Transaktion mehrere Änderungen atomar: Die Bestellzeile, ihre Positionen und die Zahlungsreferenz werden alle gespeichert oder keine (Abschnitt 0.5). Sobald die Daten auf Services verteilt sind, die jeweils ihre eigene Datenbank besitzen, wie es Microservices und Datenbank pro Service empfehlen, berührt eine einzelne Geschäftsaktion wie das Aufgeben einer Essensbestellung mehrere Datenbanken verschiedener Services: die des Bestellservice, die des Zahlungsservice und die des Dispositionsservice. Keine einzelne lokale Transaktion kann sie alle abdecken.

Es gibt zwei Familien von Antworten. Die eine besteht darin, viele Ressourcenmanager über ein verteiltes atomares Commit-Protokoll wie einen einzigen handeln zu lassen; der Klassiker dafür ist Two-Phase Commit. Die andere akzeptiert, dass die Aktion eine Folge lokaler Transaktionen ist, und plant für die Momente dazwischen: die Saga. Eine dritte Antwort, oft die beste, besteht darin zu erkennen, dass Transaktionsgrenze und Servicegrenze nicht übereinstimmen, und eine von beiden zu verschieben. Dieser Abschnitt behandelt alle drei und endet mit einer Entscheidungshilfe.

Two-Phase Commit und XA

Two-Phase Commit (2PC) ist ein Protokoll aus den späten 1970er-Jahren, das Alles-oder-nichts-Atomarität über mehrere Teilnehmer hinweg herstellt, etwa zwei Datenbanken oder eine Datenbank und einen Message Broker. Ein Koordinator (der Transaktionsmanager) führt es aus. In Phase 1, Prepare, fragt der Koordinator jeden Teilnehmer, ob er committen kann. Ein Teilnehmer, der mit Ja antwortet, muss die Änderungen zuvor dauerhaft speichern und die betreffenden Sperren halten, denn mit seinem Ja gibt er das Recht auf, eigenständig abzubrechen. Lauten alle Stimmen Ja, schreibt der Koordinator seine Entscheidung, COMMIT, in sein eigenes Log; lautet eine Stimme Nein oder antwortet ein Teilnehmer nicht, lautet die Entscheidung ABORT. In Phase 2, Commit, teilt er jedem Teilnehmer das Ergebnis mit, und diese wenden es an und geben ihre Sperren frei.

Die Schwäche: 2PC ist ein blockierendes Protokoll. Ein Teilnehmer, der mit Ja gestimmt hat, ist im Ungewissen: Er kennt das Ergebnis nicht und kann es nicht gefahrlos erraten, weil ein anderer Teilnehmer womöglich schon committet hat. Stürzt der Koordinator genau dann ab und bleibt ausgefallen, behalten die ungewissen Teilnehmer ihre Sperren, bis er sich erholt, und andere Transaktionen, die diese Zeilen berühren, warten ebenfalls. Der Betrieb kann eine heuristische Entscheidung erzwingen, um die Sperren freizugeben, riskiert damit aber, die Atomarität zu brechen.

Die XA-Spezifikation, 1991 von X/Open veröffentlicht, standardisierte die Schnittstelle zwischen einem Transaktionsmanager und Ressourcenmanagern, sodass beispielsweise ein Java-Anwendungsserver ein Datenbank-Update und das Senden einer Nachricht atomar committen kann. Viele relationale Datenbanken und einige Message Broker unterstützen sie.

Two-Phase Commit und wo es blockiert

Two-Phase Commit: Der Koordinator bittet jeden Teilnehmer um Prepare, sammelt Ja-Stimmen, protokolliert seine Entscheidung und weist jeden zum Commit anKoordinatorBestell-DBZahlungs-DBPhase 1:PreparePrepare?Prepare?Ja (hält Sperren)Ja (hält Sperren)Log: COMMITKoordinator-Absturz hierPhase 2:CommitCommitCommitfertigfertigFällt der Koordinator nach den Stimmen aus, können Teilnehmer mit Ja nicht allein entscheiden und behalten ihre Sperren
Teilnehmer, die mit Ja gestimmt haben, halten ihre Sperren, bis sie die Entscheidung erfahren; fällt der Koordinator an der markierten Stelle aus, hängen sie fest.

Warum man 2PC zwischen Microservices meidet

2PC funktioniert und wird innerhalb der Grenzen eines einzelnen Systems weiterhin eingesetzt. Über unabhängig verantwortete Services hinweg passt es meist schlecht, aus Gründen, die sich gegenseitig verstärken.

Die Verfügbarkeit multipliziert sich nach unten. Jeder Teilnehmer und der Koordinator müssen laufen, damit eine Transaktion committen kann; die Verfügbarkeit der Transaktion ist also das Produkt ihrer Verfügbarkeiten, dieselbe Rechnung wie bei einer synchronen Aufrufkette (siehe Warum verteilen). Sperren werden über das Netzwerk gehalten, für mindestens zwei Roundtrips und viel länger, wenn irgendetwas langsam ist; das senkt den Durchsatz und erhöht die Konkurrenz um Ressourcen. Die Kopplung kehrt zurück: Alle Teilnehmer müssen dasselbe Protokoll sprechen, einen Transaktionsmanager teilen und sich auf Timeouts einigen, was die unabhängige Deploybarkeit untergräbt, die die Aufteilung der Services gerechtfertigt hat. Viele Teilnehmer können gar nicht mitmachen: Die meisten HTTP-APIs, Dienste Dritter wie Zahlungsanbieter, viele NoSQL-Datenbanken und mehrere verbreitete Broker unterstützen XA nicht. Und die API des Zahlungsanbieters wird niemals eine Sperre für Ihren Koordinator halten.

Innerhalb verteilter Datenbanken sieht das Bild anders aus. Systeme wie Google Spanner führen Two-Phase Commit intern zwischen Replikatgruppen aus, die ihrerseits per Konsens konsistent gehalten werden; ein abgestürzter Koordinator wird daher ersetzt, statt dass man auf ihn wartet. Das macht partitionsübergreifende Transaktionen innerhalb einer Datenbank praktikabel; Transaktionen über getrennt verantwortete Services mit ihren unterschiedlichen Technologien macht es nicht praktikabel.

Sagas: eine Folge lokaler Transaktionen

Hector Garcia-Molina und Kenneth Salem führten die Saga 1987 in einem SIGMOD-Paper über langlebige Transaktionen ein, also solche, die Datenbanksperren stundenlang halten würden. Ihre Idee war, eine solche Transaktion in eine Folge gewöhnlicher Transaktionen T1, T2 … Tn zu zerlegen, von denen jede für sich committet, und jeder eine kompensierende Transaktion Ci zur Seite zu stellen, die sie semantisch rückgängig macht. Das System garantiert dann, dass entweder die ganze Folge abgeschlossen wird oder auf die abgeschlossenen Schritte ihre Kompensationen in umgekehrter Reihenfolge folgen: T1 … Tj, Cj … C1.

Microservice-Architekturen übernahmen die Idee, weil sie genau zu ihren Randbedingungen passt: Jeder Schritt ist eine lokale ACID-Transaktion in der eigenen Datenbank eines Service, und die Schritte sind durch Nachrichten verbunden. Eine Saga liefert Atomarität im letztendlichen, fachlichen Sinn (alle Schritte finden statt, oder ihre Wirkungen werden rückgängig gemacht), aber keine Isolation: Zwischen den Schritten können andere Transaktionen den Zwischenzustand sehen.

Es hilft, die Schritte einzuordnen, wie Chris Richardson es in Microservices Patterns (2018) tut. Kompensierbare Transaktionen lassen sich durch eine Kompensation rückgängig machen. Die Pivot-Transaktion ist der Go/No-go-Punkt: Sobald sie committet, läuft die Saga bis zum Ende. Wiederholbare Transaktionen folgen nach dem Pivot und müssen so entworfen sein, dass sie bei Wiederholung irgendwann gelingen. Schritte, die wahrscheinlich fehlschlagen, vor den Pivot zu legen und Schritte, die sich nicht rückgängig machen lassen, dahinter, ist eine zentrale Entwurfsentscheidung.

Choreografie oder Orchestrierung

Die Schritte einer Saga müssen irgendwie koordiniert werden, und die beiden Möglichkeiten entsprechen der Choreografie und Orchestrierung aus ereignisgesteuerte Architektur.

In einer choreografiebasierten Saga gibt es keinen Koordinator. Jeder Service lauscht auf Ereignisse und reagiert, indem er seine lokale Transaktion ausführt und ein neues Ereignis veröffentlicht: Der Bestellservice veröffentlicht OrderCreated, der Zahlungsservice reagiert und veröffentlicht PaymentAuthorised, der Dispositionsservice reagiert darauf und so weiter; Fehlerereignisse lösen Kompensationen in den Services aus, die auf sie lauschen. Für zwei oder drei Schritte ist das einfach und hält die Services lose gekoppelt. Mit wachsender Zahl der Schritte existiert der Ablauf nur noch implizit, verstreut über die Services, und ist daher schwer zu überblicken, zu testen und zu ändern; außerdem schleichen sich zyklische Ereignisabhängigkeiten zwischen Services ein.

In einer orchestrierungsbasierten Saga sendet ein einzelner Orchestrator, meist im Service, dem der Geschäftsprozess gehört, Commands an die Teilnehmer und reagiert auf ihre Antworten. Der Orchestrator ist ein persistenter Zustandsautomat: Er hält in seiner eigenen Datenbank fest, bei welchem Schritt er ist, und kann daher nach einem Absturz weitermachen. Der Ablauf ist explizit und an einer Stelle testbar, und die Teilnehmer müssen nichts voneinander wissen. Das Risiko besteht darin, dass sich im Orchestrator Geschäftslogik ansammelt, die in die Teilnehmer gehört. Workflow-Engines wie Temporal, Camunda und AWS Step Functions bieten dauerhafte Orchestrierung als Plattform, um den Preis einer weiteren Abhängigkeit. Bei mehr als etwa drei Schritten oder bei jedem Ablauf mit Verzweigungen und Timeouts bevorzugen die meisten Fachleute Orchestrierung.

Ausgearbeitetes Beispiel: die Bestell-Saga

Die Bestell-Saga der Essenslieferplattform wird vom Bestellservice orchestriert. Die Reihenfolge der Schritte ist bewusst gewählt: Die Zahlungsautorisierung ist eine günstige Vormerkung, die sich leicht stornieren lässt, die Reservierung eines Kuriers ist der Schritt, der zu Stoßzeiten am ehesten scheitert, und die Bestätigung an das Restaurant ist der Pivot, denn sobald eine Küche mit dem Kochen begonnen hat, lässt sich die Bestellung nicht einfach rückgängig machen.

Die Bestell-Saga Schritt für Schritt

SchrittService und lokale TransaktionArtKompensation, falls ein späterer Schritt scheitert
1. Bestellung anlegenDer Bestellservice legt die Bestellung mit dem Zustand PENDING an (eine semantische Sperre).KompensierbarC1: Zustand auf REJECTED setzen und den Grund festhalten. Die Zeile bleibt für die Prüfspur erhalten und wird nicht gelöscht.
2. Zahlung autorisierenDer Zahlungsservice bittet den Kartenanbieter, den Gesamtbetrag vorzumerken, mit der Bestell-ID als Idempotenzschlüssel.KompensierbarC2: die Autorisierung stornieren und die Vormerkung freigeben. Der Kunde sieht in seiner Banking-App eventuell kurz einen vorgemerkten Betrag.
3. Kurier reservierenDer Dispositionsservice reserviert einen Kurier in der Nähe für das Abholfenster.KompensierbarC3: den Kurier freigeben, damit er andere Aufträge übernehmen kann.
4. Bestellung bestätigenDer Bestellservice setzt den Zustand auf CONFIRMED und sendet den Bon an das Restaurant.PivotKeine: Ab hier muss die Saga vorwärtslaufen. Spätere Probleme werden durch neue Geschäftsprozesse behandelt (Erstattungen, Gutscheine).
5. Kunden benachrichtigen; Zahlung bei Abholung einziehenDer Benachrichtigungsservice sendet „Bestellung bestätigt“; der Zahlungsservice zieht den vorgemerkten Betrag ein, wenn der Kurier das Essen abholt.WiederholbarKeine: Diese Schritte werden idempotent wiederholt, bis sie gelingen oder an einen Menschen eskaliert werden.

Die Bestell-Saga, wenn kein Kurier verfügbar ist

Sequenzdiagramm: eine orchestrierte Bestell-Saga, in der die Kurierreservierung scheitert und frühere Schritte in umgekehrter Reihenfolge kompensiert werdenBestellserviceSaga-OrchestratorZahlungsserviceDisposition1. Best. PENDING2. AuthorisePaymentAuthorised3. ReserveCourierNoCourierAvailableSchritte kompensieren, neueste zuerstC2. VoidAuthorisationVoidedC1. Best. REJECTEDKunde: kein Kurier, nichts abgebucht
Schritt 3 scheitert, also führt der Orchestrator die Kompensationen für die Schritte 2 und 1 aus, die neueste zuerst, und der Kunde wird informiert, bevor etwas gekocht oder abgebucht wurde.

Kompensation ist kein Rollback

Ein Datenbank-Rollback macht es so, als hätte die Transaktion nie stattgefunden: Niemand sonst hat ihre Änderungen je gesehen. Eine Kompensation ist eine neue, vorwärtsgerichtete Transaktion, die einer früheren, die tatsächlich stattgefunden hat und möglicherweise gesehen wurde, semantisch entgegenwirkt. In der Bestell-Saga zeigt die Bank des Kunden vielleicht schon eine Vormerkung an, und eine E-Mail hat womöglich „Zahlung autorisiert“ gemeldet. Die Kompensation kann die E-Mail nicht zurückholen; sie kann nur die Vormerkung stornieren und eine weitere Nachricht senden. Manche Aktionen lassen sich überhaupt nicht kompensieren, etwa bereits gekochtes Essen oder eine bereits gelesene Benachrichtigung; sie gehören hinter den Pivot oder brauchen eine fachliche Kompensation wie einen Gutschein oder eine Erstattung.

Kompensationen brauchen daher dieselbe Sorgfalt wie Vorwärtsschritte. Sie müssen idempotent sein, weil die auslösende Nachricht zweimal zugestellt werden kann. Sie müssen wiederholbar sein, bis sie gelingen, denn eine dauerhaft scheiternde Kompensation hinterlässt das System in einem Zustand, den niemand entworfen hat; wenn sie wirklich nicht gelingen können, müssen sie einen Alarm auslösen, den ein Mensch auflöst. Sie müssen eventuell mit dem Vorwärtsschritt kommutativ sein: Trifft ein VoidAuthorisation vor dem AuthorisePayment ein, das es rückgängig macht (nach Timeouts und Wiederholungen möglich), sollte sich der Zahlungsservice die Stornierung merken und die verspätete Autorisierung ablehnen, statt zu scheitern. Und sie sollten für das Geschäft sichtbar sein: „abgelehnt, kein Kurier“ ist eine Information, die das Betriebsteam auf einem Dashboard sehen will.

Leben ohne Isolation: Anomalien und Gegenmaßnahmen

Weil jeder Schritt für sich committet, fehlt Sagas das I in ACID, und nebenläufige Sagas können sich gegenseitig stören. Richardson beschreibt drei daraus folgende Anomalien. Ein Lost Update (verlorenes Update) tritt auf, wenn eine Saga eine Änderung einer anderen überschreibt, ohne sie zu lesen, etwa wenn eine Stornierungs-Saga eine Bestellung als storniert markiert und die Bestell-Saga sie danach auf bestätigt setzt. Ein Dirty Read tritt auf, wenn eine Saga Daten liest, die eine andere Saga geändert hat, aber noch kompensieren könnte, etwa eine Kreditlimitprüfung, die eine Vormerkung mitzählt, die gleich storniert wird. Ein Fuzzy oder Non-Repeatable Read tritt auf, wenn zwei Schritte einer Saga dieselben Daten lesen und unterschiedliche Werte erhalten, weil eine andere Saga sie zwischendurch geändert hat.

Er katalogisiert außerdem Gegenmaßnahmen, Entwurfstechniken, die die Anomalien verringern, gestützt auf frühere Forschung zu Transaktionen über mehrere Datenbanken. Am weitesten verbreitet sind diese.

Gegenmaßnahmen gegen die fehlende Isolation in Sagas (nach Richardson)

GegenmaßnahmeIdeeBeispiel Essenslieferung
Semantische SperreEin kompensierbarer Schritt setzt ein Kennzeichen auf Anwendungsebene, meist einen Zustand *_PENDING, das anderen Transaktionen mitteilt, dass der Datensatz im Fluss ist; sie warten, scheitern oder behandeln ihn gesondert.Solange eine Bestellung PENDING ist, wird ein Stornierungswunsch in eine Warteschlange gestellt oder mit „Bestellung wird bearbeitet“ abgelehnt, statt mittendrin angewendet zu werden.
Kommutative UpdatesUpdates so entwerfen, dass sie in jeder Reihenfolge dasselbe Ergebnis liefern, was Lost Updates beseitigt.Das Verdienstkonto des Kuriers erfasst Gutschriften und Belastungen als getrennte Einträge, statt einen Saldo zu überschreiben.
Pessimistische SichtSchritte so umordnen, dass das geschäftliche Risiko eines Dirty Reads minimal wird, indem riskante Updates in wiederholbare Schritte nach dem Pivot gelegt werden.Die „verfügbare Kapazität“ eines Restaurants nur in einem wiederholbaren Schritt erhöhen, nachdem die Stornierung feststeht, nie in einem Schritt, der kompensiert werden könnte.
Wert erneut lesenVor dem Update den Datensatz erneut lesen und prüfen, dass er sich seit dem Lesen nicht geändert hat (optimistisches Sperren); falls doch, abbrechen und die Saga eventuell neu starten.Vor der Bestätigung prüft der Orchestrator, dass die Version der Bestellung noch die von ihm angelegte ist; hat eine Stornierung sie geändert, kompensiert er stattdessen.
VersionsdateiDie auf einen Datensatz angewendeten Operationen festhalten, damit Operationen, die in falscher Reihenfolge eintreffen, umgeordnet oder erkannt werden können.Der Zahlungsservice hält fest, dass eine Stornierung für eine Autorisierung eingetroffen ist, die er noch nicht gesehen hat, und lehnt die verspätete Autorisierung ab.
Nach WertDen Koordinationsmechanismus pro Anfrage nach ihrem geschäftlichen Risiko wählen.Gewöhnliche Bestellungen nutzen die Saga; ein hochwertiger Catering-Auftrag eines Unternehmens könnte einen strengeren, langsameren Prozess durchlaufen.

Zuverlässig veröffentlichen: die Transactional Outbox

Jeder Saga-Schritt tut zwei Dinge: Er aktualisiert seine eigene Datenbank und sendet eine Nachricht (ein Ereignis oder eine Antwort). Beides getrennt zu tun, erzeugt das Dual-Write-Problem. Committet der Service und stürzt dann vor dem Veröffentlichen ab, erfährt der Rest der Saga nie von dem Schritt. Veröffentlicht er zuerst und scheitert dann der Commit, handeln andere Services auf Grundlage von etwas, das nie geschehen ist. Datenbank und Broker in 2PC einzubinden, ist genau das, was wir vermeiden wollten.

Die Transactional Outbox löst das mit einer lokalen Transaktion. Der Service schreibt die fachliche Änderung und die ausgehende Nachricht, als Zeile in einer outbox-Tabelle, in derselben Datenbanktransaktion; entweder committen beide oder keines. Ein separates Relay liest dann die Outbox und veröffentlicht jede Nachricht beim Broker. Zwei Relay-Entwürfe sind verbreitet. Ein Polling Publisher fragt die Outbox-Tabelle in Abständen nach unveröffentlichten Zeilen ab und markiert sie als gesendet: einfach, erzeugt aber Last und Latenz. Transaction Log Tailing nutzt Change Data Capture (CDC): Ein Tool liest das datenbankeigene Replikationslog (das Write-Ahead-Log von PostgreSQL oder das Binary Log von MySQL) und macht aus neuen Outbox-Zeilen Nachrichten. Debezium, eine Open-Source-CDC-Plattform, die meist mit Kafka Connect betrieben wird, ist das bekannteste Beispiel und enthält einen Outbox Event Router, der jede Zeile anhand einer Spalte wie dem Aggregattyp an ein Topic leitet.

Das Relay kann nach dem Veröffentlichen und vor dem Vermerken abstürzen und veröffentlicht daher manchmal eine Nachricht zweimal. Die Outbox liefert also At-least-once-Zustellung, und Consumer müssen mit Duplikaten zurechtkommen. Die Reihenfolge der Veröffentlichung bleibt pro Aggregat erhalten, wenn das Relay die Zeilen in Commit-Reihenfolge veröffentlicht und die Aggregat-ID als Nachrichtenschlüssel verwendet.

BEGIN;

INSERT INTO orders (id, customer_id, state, total_pence)
VALUES ('1042', 'c-77', 'PENDING', 2350);

INSERT INTO outbox (id, aggregate_type, aggregate_id, type, payload)
VALUES ('e-9f1c', 'order', '1042', 'OrderCreated',
        '{"orderId":"1042","customerId":"c-77","totalPence":2350}');

COMMIT;
-- A relay (poller or CDC such as Debezium) later publishes e-9f1c to the broker.
Eine Bestellung mit Transactional Outbox aufgeben: Die Bestellung und ihr Ereignis werden gemeinsam in einer lokalen Transaktion committet.

Outbox auf dem Hinweg, Inbox auf dem Rückweg

Die Transactional Outbox: Bestellung und Ereignis werden in einer lokalen Transaktion geschrieben, ein Relay veröffentlicht das Ereignis, und der Consumer dedupliziert mit einer InboxBestellserviceBestellen: eine lokale TransaktionordersZeile 1042outboxOrderPlacedbeide committenoder keineRelayPoller oder CDCliestBrokerTopic: orderspubliziertDispositionInbox: Nachr.-IDsAt-least-once-Zustellung;Duplikate ignoriert
Die Outbox beseitigt den Dual Write auf der Senderseite; die Inbox oder eine andere Duplikatprüfung macht die Empfängerseite bei At-least-once-Zustellung sicher.

Zuverlässig empfangen: idempotente Empfänger und die Inbox

Weil die Outbox, Wiederholungen des Brokers und Wiederholungen der Saga allesamt Duplikate erzeugen, muss jeder Teilnehmer ein idempotenter Empfänger sein, so der Name, den Gregor Hohpe und Bobby Woolf dem Muster in Enterprise Integration Patterns (2003) gaben. Manche Operationen sind von Natur aus idempotent („setze Bestellung 1042 auf CONFIRMED“); andere brauchen eine Duplikatprüfung. Die übliche Umsetzung ist eine Inbox: eine Tabelle verarbeiteter Nachrichten-IDs, geschrieben in derselben lokalen Transaktion wie die fachliche Änderung, sodass „die Nachricht verarbeiten“ und „sich merken, dass sie verarbeitet wurde“ gemeinsam committen. Eine erneut zugestellte Nachricht findet ihre ID bereits vor und wird bestätigt, ohne dass etwas geschieht. Der Code auf Consumer-Seite steht in ereignisgesteuerte Architektur.

Zusammen liefern Outbox, At-least-once-Zustellung und idempotente Empfänger das praktische Ergebnis, das meist gemeint ist, wenn von „Exactly-once“ die Rede ist: Jede fachliche Wirkung tritt einmal ein, auch wenn Nachrichten mehr als einmal unterwegs sind.

Entscheidungshilfe

Entscheidungshilfe: eine Transaktion, eine Saga oder neue Grenzen?

Nutzen Sie diese Hilfe, wenn eine Geschäftsaktion Daten an mehr als einer Stelle ändern muss.

  1. Gehören alle Daten, die sich gemeinsam ändern müssen, einem Service und liegen sie in einer Datenbank?
    JaVerwenden Sie eine lokale ACID-Transaktion. Das ist die einfachste und stärkste Option; verteilen Sie sie nicht.
    NeinWeiter zur nächsten Frage.
  2. Ändern sich die Teile, die sich gemeinsam ändern müssen, fast immer gemeinsam, gehören sie einem Team und teilen sie einen Bounded Context, sodass die Aufteilung zufällig ist?
    JaGestalten Sie die Grenze neu: Führen Sie die Services zusammen oder verschieben Sie die Daten, sodass die Aktion zu einer lokalen Transaktion wird. Sagas sind keine Reparatur für eine falsche Aufteilung.
    NeinWeiter zur nächsten Frage.
  3. Muss jeder Teilnehmer alle Änderungen im selben Augenblick sehen, ohne dass je ein Zwischenzustand sichtbar ist (zum Beispiel ein Hauptbuch, das immer ausgeglichen sein muss)?
    JaHalten Sie diese Datensätze in einer Datenbank, oder verwenden Sie eine verteilte Datenbank, die intern partitionsübergreifende Transaktionen bietet. Setzen Sie 2PC nur innerhalb der Grenzen eines Systems ein, mit Teilnehmern, die Sie kontrollieren.
    NeinWeiter zur nächsten Frage.
  4. Lässt sich jeder Schritt entweder kompensieren oder hinter einen Pivot legen und wiederholen, bis er gelingt?
    JaVerwenden Sie eine Saga: orchestriert, wenn sie mehr als etwa drei Schritte oder Verzweigungen hat, choreografiert, wenn sie kurz und stabil ist. Ergänzen Sie eine Outbox, idempotente Empfänger und Gegenmaßnahmen wie semantische Sperren.
    NeinOrdnen Sie die Schritte um oder ändern Sie den Geschäftsprozess (zum Beispiel reservieren, bevor belastet wird, oder eine manuelle Prüfung einfügen), bis es geht; andernfalls überdenken Sie die Grenzen.

Prüfen Sie Ihr Verständnis

Warum kann ein Teilnehmer an einem Two-Phase Commit nicht einfach selbst committen, wenn der Koordinator verschwindet, nachdem er mit Ja gestimmt hat?

Weil er die Entscheidung nicht kennt: Ein anderer Teilnehmer hat vielleicht mit Nein gestimmt, sodass der Koordinator womöglich den Abbruch beschlossen hat; Raten könnte die Atomarität brechen.

Warum ist in der Bestell-Saga die Bestätigung an das Restaurant der Pivot und nicht der erste Schritt?

Sobald die Küche zu kochen beginnt, lässt sich der Schritt nicht rückgängig machen; er muss daher nach den Schritten kommen, die scheitern könnten (Zahlungsautorisierung, Kurierreservierung) und sich günstig kompensieren lassen.

Welches Problem löst die Transactional Outbox, und welche Garantie gibt sie?

Sie beseitigt den Dual Write zwischen Datenbank und Broker, indem sie Änderung und Nachricht gemeinsam committet; das Relay stellt die Nachricht dann mindestens einmal zu, Duplikate sind also möglich.

Nennen Sie zwei Unterschiede zwischen einer Kompensation und einem Datenbank-Rollback.

Sie ist eine neue Transaktion, die läuft, nachdem das Original committet wurde, und andere haben das Original womöglich schon gesehen oder darauf reagiert; außerdem ist sie unter Umständen nur ein semantisches Rückgängigmachen (eine Erstattung, eine Stornierung), statt den exakten vorherigen Zustand wiederherzustellen.

Weiter · 6.8Serverless und Space-BasedZwei gegensätzliche Antworten auf die Kapazitätsfrage: Serverless-Funktionen und verwaltete Backends, die auf null skalieren und nach Nutzung abrechnen, und Space-Based Architecture, die extreme Spitzen aus repliziertem Arbeitsspeicher bedient und die Datenbank aus dem heißen Pfad nimmt.Weiter →

Quellen und weiterführende Literatur

Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.

Fragen: „Unser Checkout ruft erst die Zahlung, dann den Lagerbestand, dann den Versand auf, und manchmal belasten wir Kunden für Artikel, die wir nicht versenden können. Wie sollten wir ihn als Saga neu gestalten?“