konsistente Ablaeufe ohne globale Locks
Sobald eine Geschaeftsoperation Daten mehrerer Microservices mit eigenen Datenbanken aendert, funktioniert eine klassische ACID-Transaktion nicht mehr. Verteilte Transaktionen ueber Services hinweg loesen dieses Problem nicht durch globale Sperren, sondern durch das Saga Pattern: eine Kette lokaler Transaktionen mit definierten Kompensationsschritten fuer den Fehlerfall.
Inhaltsverzeichnis
- 1. Warum ACID an Service-Grenzen scheitert
- 2. Das Saga Pattern als Alternative zu globalen Locks
- 3. Choreographie vs. Orchestrierung von Sagas
- 4. Kompensationslogik richtig entwerfen
- 5. Das Outbox Pattern fuer zuverlaessige Events
- 6. Idempotenz als Voraussetzung fuer Sagas
- 7. Eventual Consistency in der Anwendung sichtbar machen
- 8. Monitoring und Debugging verteilter Sagas
- 9. Saga Pattern im Vergleich zu anderen Ansaetzen
- 10. Zusammenfassung
- 11. FAQ
1. Warum ACID an Service-Grenzen scheitert
Eine klassische Datenbanktransaktion garantiert Atomaritaet innerhalb einer einzigen Datenbank: entweder werden alle Aenderungen uebernommen, oder keine. Sobald eine Geschaeftsoperation, etwa eine Bestellung, gleichzeitig den Bestand im Warenwirtschaftssystem reduziert, eine Zahlung im Payment-Service autorisiert und eine Versandanfrage im Logistik-Service erstellt, greift diese Garantie nicht mehr, weil jeder Service seine eigene Datenbank besitzt. Verteilte Transaktionen ueber Services hinweg muessen dieses Problem loesen, ohne die Datenbanken der einzelnen Services zusammenzuschliessen.
Der naheliegende, aber problematische Ansatz ist Two-Phase-Commit ueber alle beteiligten Datenbanken hinweg. Dieser Mechanismus haelt waehrend der gesamten Operation Sperren auf allen beteiligten Ressourcen, was in einem verteilten System mit unabhaengig deploybaren Services zu massiven Verfuegbarkeitsproblemen fuehrt: faellt ein einzelner Service waehrend der Sperrzeit aus, blockieren alle anderen beteiligten Services, bis das Problem manuell geloest wird.
Das Saga Pattern loest dieses Dilemma anders: statt einer einzigen atomaren Operation ueber alle Services hinweg besteht eine Saga aus einer Sequenz unabhaengiger, lokaler Transaktionen, jede innerhalb ihres eigenen Service mit eigener ACID-Garantie. Schlaegt ein Schritt fehl, werden bereits abgeschlossene Schritte durch explizite Kompensationsaktionen rueckgaengig gemacht, statt auf eine globale Sperre zu vertrauen, die es in diesem Modell gar nicht gibt.
2. Das Saga Pattern als Alternative zu globalen Locks
Eine Saga modelliert eine Geschaeftsoperation als geordnete Folge lokaler Transaktionen T1, T2, T3 bis Tn, wobei jede Transaktion in einem anderen Service laeuft. Zu jeder Transaktion Ti gehoert eine Kompensationstransaktion Ci, die die Wirkung von Ti rueckgaengig macht, falls ein spaeterer Schritt in der Kette fehlschlaegt. Bei einer Bestellung koennte das bedeuten: T1 reserviert Bestand, T2 autorisiert Zahlung, T3 erstellt Versandauftrag. Schlaegt T3 fehl, laeuft C2 (Zahlungsautorisierung stornieren) und dann C1 (Bestandsreservierung freigeben).
Der zentrale konzeptuelle Unterschied zu klassischen Transaktionen: waehrend einer Saga sind Zwischenzustaende fuer andere Teile des Systems sichtbar. Zwischen T1 und T2 existiert ein Zustand, in dem Bestand reserviert, aber Zahlung noch nicht autorisiert ist, ein Zustand, der bei einer klassischen Transaktion niemals nach aussen sichtbar waere. Diese Sichtbarkeit von Zwischenzustaenden ist der Preis, den verteilte Transaktionen im Saga-Modell zahlen, im Austausch fuer die Vermeidung globaler Sperren.
Sagas garantieren keine Isolation im klassischen ACID-Sinn. Zwei gleichzeitig laufende Sagas koennen sich theoretisch gegenseitig beeinflussen, etwa wenn beide denselben knappen Lagerbestand reservieren wollen. Diese fehlende Isolation muss durch anwendungsspezifische Massnahmen kompensiert werden, etwa durch pessimistische Reservierungen auf Service-Ebene oder durch das explizite Akzeptieren gelegentlicher Konflikte, die dann ueber Kompensation aufgeloest werden.
3. Choreographie vs. Orchestrierung von Sagas
Zwei grundsaetzliche Implementierungsstile existieren fuer Sagas. Bei der Choreographie reagiert jeder Service auf Events, die von anderen Services ausgeloest werden, und loest selbst wieder neue Events aus, ohne eine zentrale Steuerungsinstanz. Der Bestellservice loest ein "OrderCreated"-Event aus, der Payment-Service reagiert darauf mit einer Zahlungsautorisierung und loest "PaymentAuthorized" aus, der Logistik-Service reagiert wiederum darauf. Diese Entkopplung ist elegant fuer einfache Ablaeufe, wird aber bei komplexen Sagas mit vielen bedingten Verzweigungen schwer nachvollziehbar, weil der Gesamtablauf ueber viele Service-Implementierungen verteilt ist.
Bei der Orchestrierung uebernimmt ein zentraler Saga-Orchestrator die explizite Steuerung: er ruft jeden Service in der richtigen Reihenfolge auf, wartet auf Ergebnisse und entscheidet bei Fehlern, welche Kompensationsschritte in welcher Reihenfolge ausgeloest werden. Der Ablauf ist an einer einzigen Stelle im Code sichtbar, was Debugging und Nachvollziehbarkeit deutlich vereinfacht, allerdings auf Kosten einer zusaetzlichen, zentralen Komponente, die selbst hochverfuegbar betrieben werden muss.
Fuer die meisten produktiven Systeme mit mehr als drei oder vier beteiligten Services empfiehlt sich Orchestrierung, weil die explizite Ablaufsteuerung die Komplexitaet handhabbarer macht, sobald Fehlerbehandlung und bedingte Logik ins Spiel kommen. Choreographie eignet sich gut fuer einfache, lineare Ablaeufe mit wenigen Beteiligten, bei denen die Entkopplung mehr Vorteil bringt als die fehlende zentrale Uebersicht kostet.
-- Beispiel: Saga-Zustand in einer Orchestrator-Tabelle nachverfolgen
CREATE TABLE saga_instances (
saga_id UUID PRIMARY KEY,
saga_type VARCHAR(100) NOT NULL,
current_step VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL, -- RUNNING, COMPLETED, COMPENSATING, FAILED
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Fortschritt einer Saga aktualisieren
UPDATE saga_instances
SET current_step = 'payment_authorized',
status = 'RUNNING',
updated_at = now()
WHERE saga_id = '3f29a1c4-...';
-- Fehlgeschlagene Sagas fuer Kompensation identifizieren
SELECT saga_id, current_step, payload
FROM saga_instances
WHERE status = 'FAILED'
ORDER BY updated_at ASC;
4. Kompensationslogik richtig entwerfen
Eine Kompensationstransaktion ist keine echte Rueckabwicklung im Datenbanksinn, sondern eine semantische Umkehrung. Eine Zahlungsautorisierung wird nicht geloescht, sondern durch eine Stornobuchung ausgeglichen, weil die urspruengliche Buchung aus Audit- und Compliance-Gruenden erhalten bleiben muss. Diese semantische statt technische Umkehrung ist ein grundlegender Unterschied zum Rollback einer klassischen Transaktion und muss beim Entwurf jeder Kompensation explizit mitgedacht werden.
Nicht jede Aktion ist kompensierbar. Eine E-Mail-Benachrichtigung, die bereits an einen Kunden verschickt wurde, kann nicht zurueckgeholt werden. Fuer solche nicht-kompensierbaren Schritte gilt die Regel, sie moeglichst spaet in der Saga zu platzieren, idealerweise erst, nachdem alle anderen, kompensierbaren Schritte erfolgreich abgeschlossen sind. Ein Bestaetigungs-Email sollte deshalb der letzte Schritt einer Bestell-Saga sein, nicht einer der ersten.
Kompensationsschritte muessen selbst robust gegenueber Fehlern sein, denn ein fehlschlagender Kompensationsschritt ist ein noch schwerer zu behebendes Problem als der urspruengliche Fehler. In der Praxis bedeutet das: Kompensationslogik mit eigenen Retry-Mechanismen ausstatten und Faelle, in denen auch die Kompensation wiederholt fehlschlaegt, in eine manuelle Bearbeitungswarteschlange ueberfuehren, statt sie stillschweigend zu verwerfen.
5. Das Outbox Pattern fuer zuverlaessige Events
Ein subtiles, aber haeufiges Problem bei choreographierten Sagas ist die Konsistenz zwischen einer lokalen Datenbankaenderung und dem Versenden des dazugehoerigen Events. Schreibt ein Service zuerst in seine Datenbank und sendet danach ein Event an einen Message-Broker, kann zwischen diesen beiden Schritten ein Ausfall auftreten: die Datenbankaenderung ist committed, aber das Event wurde nie gesendet, weil der Prozess vorher abstuerzte. Andere Services erfahren nie von der Aenderung, und die Saga bleibt in einem inkonsistenten Zwischenzustand haengen.
Das Outbox Pattern loest dieses Problem, indem es die Event-Erzeugung Teil derselben lokalen Datenbanktransaktion macht wie die eigentliche Geschaeftsdatenaenderung. Statt das Event direkt an den Broker zu senden, wird ein Eintrag in einer Outbox-Tabelle innerhalb derselben Transaktion geschrieben. Ein separater Prozess liest diese Outbox-Tabelle kontinuierlich aus und publiziert die Events zuverlaessig an den Message-Broker, mit Wiederholungslogik bei Uebertragungsfehlern.
Damit wird die Atomaritaet zwischen Datenbankaenderung und Event-Versand auf die lokale, bereits vorhandene ACID-Garantie der einzelnen Service-Datenbank zurueckgefuehrt, ohne verteilte Transaktionen ueber Datenbank und Message-Broker hinweg zu benoetigen. Das Outbox Pattern ist damit ein zentraler Baustein fuer zuverlaessige verteilte Transaktionen in choreographierten wie orchestrierten Sagas gleichermassen.
-- Outbox-Tabelle: Event-Erzeugung als Teil derselben lokalen Transaktion
CREATE TABLE outbox_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_type VARCHAR(100) NOT NULL,
aggregate_id VARCHAR(100) NOT NULL,
event_type VARCHAR(100) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
-- Beide Schreibvorgaenge in derselben ACID-Transaktion
BEGIN;
UPDATE orders SET status = 'confirmed' WHERE id = 4821;
INSERT INTO outbox_events (aggregate_type, aggregate_id, event_type, payload)
VALUES ('order', '4821', 'OrderConfirmed', '{"order_id": 4821}'::jsonb);
COMMIT;
-- Separater Publisher-Prozess liest unveroeffentlichte Events
SELECT id, event_type, payload
FROM outbox_events
WHERE published_at IS NULL
ORDER BY created_at ASC
LIMIT 100;
6. Idempotenz als Voraussetzung fuer Sagas
Weil Netzwerke unzuverlaessig sind und Message-Broker Nachrichten mindestens einmal, gelegentlich sogar mehrfach zustellen, muss jeder Schritt einer Saga idempotent sein: mehrfache Ausfuehrung derselben Aktion darf nicht zu einem anderen Ergebnis fuehren als eine einzelne Ausfuehrung. Eine Zahlungsautorisierung, die bei doppelter Zustellung zweimal den Betrag abbucht, ist ein klassisches Beispiel fuer fehlende Idempotenz mit direkter finanzieller Konsequenz.
Der uebliche Mechanismus fuer Idempotenz ist ein eindeutiger Idempotenz-Schluessel, der mit jeder Anfrage mitgesendet wird. Der empfangende Service prueft vor der Ausfuehrung, ob dieser Schluessel bereits verarbeitet wurde, und gibt im Wiederholungsfall das zuvor berechnete Ergebnis zurueck, ohne die Aktion erneut auszufuehren. Dieser Schluessel sollte in einer eigenen Tabelle mit einem Unique-Constraint gespeichert werden, damit auch bei gleichzeitigen, doppelten Anfragen keine Race Condition entsteht.
Idempotenz muss auch fuer Kompensationsschritte gelten, nicht nur fuer die urspruengliche Aktion. Ein Kompensationsschritt, der bei Wiederholung faelschlicherweise doppelt storniert, kann ebenso zu inkorrekten Zustaenden fuehren wie eine nicht-idempotente urspruengliche Aktion. Diese Symmetrie wird in der Praxis haeufig uebersehen, weil Entwickler den Kompensationspfad seltener testen als den Erfolgspfad einer Saga.
7. Eventual Consistency in der Anwendung sichtbar machen
Sagas liefern per Definition nur Eventual Consistency: nach Abschluss aller Schritte ist das System konsistent, waehrend der Ausfuehrung existieren Zwischenzustaende, die anderen Teilen des Systems sichtbar sein koennen. Eine Anwendung, die diesen Unterschied gegenueber Nutzern verschleiert, produziert verwirrende Erlebnisse: eine Bestellung, die im UI als "bestaetigt" angezeigt wird, obwohl die Zahlungsautorisierung noch nicht abgeschlossen ist, kann spaeter durch eine Kompensation storniert werden muessen, was fuer den Nutzer unerwartet und schwer nachvollziehbar wirkt.
Eine bewaehrte Praxis ist es, den Saga-Status explizit im Datenmodell und in der Benutzeroberflaeche abzubilden, etwa als "in Bearbeitung", statt so zu tun, als waere die Operation bereits vollstaendig abgeschlossen. Das macht Eventual Consistency fuer Nutzer nachvollziehbar, statt sie zu verstecken und spaeter durch ueberraschende Korrekturen sichtbar werden zu lassen.
Fuer interne Systeme, die auf konsistente Daten angewiesen sind, etwa Reporting oder Analytics, sollte klar definiert sein, wie lange nach Abschluss einer Saga mit Konsistenz zu rechnen ist. Diese Zeitspanne haengt von der Anzahl der Schritte, der Zuverlaessigkeit des Message-Brokers und der Retry-Konfiguration ab und sollte gemessen, nicht angenommen werden.
8. Monitoring und Debugging verteilter Sagas
Verteilte Transaktionen ueber mehrere Services hinweg sind erheblich schwerer zu debuggen als eine einzelne Datenbanktransaktion, weil der relevante Zustand ueber mehrere Systeme verteilt ist. Distributed Tracing mit einer durchgaengigen Correlation-ID, die durch alle Schritte einer Saga propagiert wird, ist keine optionale Ergaenzung, sondern eine Grundvoraussetzung, um im Fehlerfall den vollstaendigen Ablauf ueber Service-Grenzen hinweg zu rekonstruieren.
Ein dediziertes Saga-Status-Dashboard, das laufende, abgeschlossene und fehlgeschlagene Sagas anzeigt, ist fuer den Betrieb produktiver Systeme mit Saga-Pattern praktisch unverzichtbar. Ohne diese Sichtbarkeit bleiben haengengebliebene Sagas, die weder erfolgreich abgeschlossen noch vollstaendig kompensiert wurden, oft ueber Tage unbemerkt, bis ein Nutzer sich ueber einen inkonsistenten Zustand beschwert.
Alarme sollten auf Sagas reagieren, die laenger als erwartet im Zustand "laeuft" oder "kompensiert gerade" verharren. Ein solches Verharren deutet meist auf einen ausgefallenen nachgelagerten Service oder eine fehlerhafte Kompensationslogik hin, die manuelles Eingreifen erfordert, bevor der betroffene Datensatz dauerhaft in einem inkonsistenten Zustand bleibt.
9. Saga Pattern im Vergleich zu anderen Ansaetzen
Die folgende Tabelle vergleicht das Saga Pattern mit alternativen Ansaetzen fuer verteilte Transaktionen ueber Services hinweg und zeigt, wann welcher Ansatz sinnvoll ist.
| Ansatz | Verfuegbarkeit | Konsistenzmodell | Praxis-Empfehlung |
|---|---|---|---|
| Two-Phase-Commit | Gering, Sperren blockieren alle Beteiligten | Stark konsistent | Nur innerhalb eines Systems, nicht ueber Services |
| Saga (Choreographie) | Hoch, keine globalen Sperren | Eventual Consistency | Fuer einfache, lineare Ablaeufe |
| Saga (Orchestrierung) | Hoch, keine globalen Sperren | Eventual Consistency | Fuer komplexe Ablaeufe mit vielen Services |
| Ein monolithisches System | Abhaengig von einer Datenbank | Stark konsistent per ACID | Wenn Services nicht wirklich noetig sind |
In der Praxis ist die wichtigste Erkenntnis, dass verteilte Transaktionen ueber Services hinweg nur dann sinnvoll sind, wenn die Aufteilung in separate Services aus fachlichen oder organisatorischen Gruenden tatsaechlich noetig ist. Wo ein monolithisches System mit einer einzigen Datenbank ausreicht, ist die zusaetzliche Komplexitaet von Sagas, Kompensation und Outbox Pattern schlicht nicht gerechtfertigt.
Mironsoft
Microservices-Architektur und verteilte Transaktionen
Geschaeftsablaeufe ueber mehrere Services hinweg?
Wir entwerfen Sagas mit klarer Kompensationslogik, richten das Outbox Pattern fuer zuverlaessige Events ein und sorgen dafuer, dass Eventual Consistency fuer Nutzer und Betrieb nachvollziehbar bleibt.
Saga-Design
Choreographie oder Orchestrierung passend zur Komplexitaet waehlen
Outbox Pattern
Zuverlaessige Events ohne verteilte Transaktionen ueber Broker hinweg
Monitoring
Distributed Tracing und Saga-Dashboards fuer schnelle Diagnose
10. Zusammenfassung
Verteilte Transaktionen ueber Services hinweg erfordern einen fundamental anderen Ansatz als klassische ACID-Transaktionen. Das Saga Pattern ersetzt globale Sperren durch eine Kette lokaler Transaktionen mit expliziter Kompensationslogik fuer den Fehlerfall. Die Wahl zwischen Choreographie und Orchestrierung haengt von der Komplexitaet des Ablaufs ab, waehrend das Outbox Pattern die zuverlaessige Zustellung von Events an die lokale ACID-Garantie jedes Services koppelt.
Idempotenz ist keine Nebensaechlichkeit, sondern Grundvoraussetzung fuer jede Saga, sowohl fuer die urspruengliche Aktion als auch fuer Kompensationsschritte. Wer diese Bausteine sauber kombiniert und Eventual Consistency fuer Nutzer und Betrieb sichtbar macht, statt sie zu verschleiern, erreicht konsistente Geschaeftsablaeufe ueber Service-Grenzen hinweg, ohne die Verfuegbarkeitsprobleme klassischer verteilter Transaktionsmechanismen wie Two-Phase-Commit.
Verteilte Transaktionen ueber Services hinweg: Das Wichtigste auf einen Blick
Saga Pattern
Kette lokaler Transaktionen mit Kompensation statt globaler Sperren ueber Service-Grenzen hinweg.
Choreographie vs. Orchestrierung
Choreographie fuer einfache Ablaeufe, Orchestrierung fuer komplexe Sagas mit vielen Services.
Outbox Pattern
Event-Erzeugung als Teil der lokalen Transaktion, kein separates Broker-Konsistenzproblem.
Idempotenz
Pflicht fuer jeden Saga-Schritt und jede Kompensation, wegen mindestens einmaliger Nachrichtenzustellung.