Das Saga-Muster: Verteilte Transaktionen in der Microservices-Architektur
In herkömmlichen monolithischen Anwendungen ist die Aufrechterhaltung der Datenkonsistenz über mehrere Einheiten hinweg unkompliziert. Relationale Datenbank-Engines bieten ACID-Garantien (Atomizität, Konsistenz, Isolation, Haltbarkeit), die in lokale SQL-Transaktionen eingebettet sind. Wenn eine Auftragserteilung, ein Zahlungsabzug oder eine Lagerbestandsreserve auf halbem Weg fehlschlägt, wird durch den Aufruf von ROLLBACK jede Datenbankänderung sofort rückgängig gemacht.
Bei der Migration auf eine moderne Microservices-Architektur verändert sich das Datenmanagement jedoch grundlegend. Um Domänenautonomie und unabhängige Skalierbarkeit sicherzustellen, besitzt jeder Microservice seine private Datenbank. Ein einzelner Geschäftsvorgang – wie die Verarbeitung einer E-Commerce-Kaufabwicklung – erstreckt sich jetzt über mehrere Servicegrenzen und Datenbank-Engines (z. B. PostgreSQL für Bestellungen, DynamoDB für Zahlungen, Redis für Inventar).
Da sich verteilte Mikrodienste nicht auf eine einzelne Datenbanktransaktion verlassen können, wird die Aufrechterhaltung der Datenkonsistenz über Netzwerkgrenzen hinweg zu einem der größten Probleme bei der Entwicklung verteilter Systeme.
Um dieses Problem zu lösen, ohne die Systemverfügbarkeit oder -leistung zu beeinträchtigen, verlassen sich Softwarearchitekten auf das Saga-Muster.
In diesem ausführlichen Tauchgang werden wir untersuchen, warum traditionelle verteilte Transaktionen scheitern, die Kernmechanismen des Saga-Musters aufschlüsseln, Choreografie mit Orchestrierung vergleichen, Isolations-Gegenmaßnahmen analysieren, Produktionscode-Implementierungen in Go und Java untersuchen und lernen, wie man Rollbacks bei realen Fehlern sicher handhabt.
Das Grundproblem: Warum Two-Phase Commit (2PC) in Microservices fehlschlägt
Vor der Einführung des Saga-Musters fragen sich Ingenieure oft: Warum können wir in unseren Microservices nicht das traditionelle Two-Phase-Commit (2PC / XA) verwenden?
Die Mechanik von 2PC
Two-Phase Commit koordiniert eine verteilte Transaktion über mehrere Datenbankknoten hinweg mithilfe eines zentralen Transaktionsmanagers in zwei Phasen:
- Vorbereitungsphase: Der Transaktionsmanager fordert alle teilnehmenden Datenbankknoten auf, die erforderlichen Zeilen vorzubereiten und zu sperren. Die Teilnehmer stimmen mit
YESoderNOab. - Commit-Phase: Wenn alle Teilnehmer für
YESgestimmt haben, gibt der Manager einenCOMMIT-Befehl an alle Knoten aus. Wenn ein Knoten fürNOgestimmt hat oder eine Zeitüberschreitung auftritt, gibt er einenROLLBACKaus.
Client ----> Transaction Manager
|
+-------------+-------------+
| (Prepare) | (Prepare) | (Prepare)
v v v
Order DB Payment DB Inventory DB
Warum 2PC ein Anti-Pattern für Microservices ist
Während 2PC eine starke Konsistenz garantiert, bricht es in Cloud-nativen Microservice-Umgebungen aufgrund mehrerer Architekturmängel zusammen:
- Blockierung und Ressourcenkonflikt: Datenbankzeilen bleiben während des mehrstufigen Netzwerk-Handshakes gesperrt. Wenn die Netzwerklatenz zunimmt oder ein Dienst langsamer wird, werden Sperren offen gehalten, wodurch Verbindungspools schnell verbraucht werden und es zu kaskadierenden Systemausfällen kommt.
- Verfügbarkeitsengpass: In 2PC ist die Systemverfügbarkeit durch das Produkt aller Teilnehmerverfügbarkeiten begrenzt ($A_{total} = A_1 \times A_2 \times \dots \times A_n$). Wenn ein einzelner Dienst oder Datenbankknoten während der Vorbereitungsphase offline geht, wird die gesamte globale Transaktion auf unbestimmte Zeit blockiert.
- Verlust der Dienstautonomie: 2PC zwingt Dienste dazu, XA-Protokolle auf Datenbankebene über Netzwerk-APIs verfügbar zu machen und ihre Datenbank-Engines direkt zu koppeln.
- Broker-Einschränkungen: Nachrichtenbroker mit hohem Durchsatz wie Apache Kafka oder RabbitMQ nehmen nicht nativ an herkömmlichen XA 2PC-Transaktionen über relationale Datenbanken hinweg teil.
Nach dem CAP-Theorem müssen verteilte Systeme unter Netzwerkpartitionen (P) zwischen starker Konsistenz (C) und hoher Verfügbarkeit (A) wählen. Moderne Microservice-Systeme priorisieren Verfügbarkeit und Partitionstoleranz und tauschen sofortige Konsistenz gegen eventuelle Konsistenz (BASIS: Grundsätzlich verfügbar, Soft-State, eventuelle Konsistenz).
Was ist das Saga-Muster?
Das Saga-Muster wurde ursprünglich 1987 von Hector Garcia-Molina und Kenneth Salem als Mechanismus zur Handhabung langlebiger Transaktionen in Datenbankverwaltungssystemen vorgeschlagen. In modernen Microservices stellt eine Saga eine Folge diskreter lokaler Transaktionen dar.
Anstatt den gesamten Multi-Service-Ablauf in eine einzige globale Sperre zu packen, führt ein Saga einen Geschäftsprozess als eine Reihe unabhängiger lokaler Datenbanktransaktionen ($T_1, T_2, \dots, T_n$) aus.
Jede lokale Transaktion aktualisiert Daten in der Datenbank eines einzelnen Microservices und veröffentlicht ein Domänenereignis oder eine Domänennachricht. Dieses Ereignis löst die nächste lokale Transaktion ($T_{i+1}$) im Downstream-Dienst aus.
[Order Service] [Payment Service] [Inventory Service]
Local Tx T1 -------------> Local Tx T2 -------------> Local Tx T3
(Create Order) (Process Payment) (Reserve Stock)
Vorwärtsausführung vs. Rückwärtskompensation
Wenn alle lokalen Transaktionen erfolgreich sind, wird die Saga erfolgreich abgeschlossen (Forward Execution).
Wenn jedoch eine lokale Transaktion auf halbem Weg fehlschlägt (z. B. $T_3$ schlägt fehl, weil ein Artikel nicht vorrätig ist), kann die Saga nicht einfach eine Datenbank ROLLBACK für vorherige Schritte ($T_1, T_2$) aufrufen, da diese lokalen Transaktionen bereits in ihren jeweiligen Datenbanken festgeschrieben wurden.
Um bereits festgeschriebene lokale Transaktionen rückgängig zu machen, muss die Saga Kompensationstransaktionen ($C_{n-1}, \dots, C_1$) in umgekehrter Reihenfolge ausführen.
Forward Flow: T1 (Create Order) ---> T2 (Charge Card) ---> T3 (Reserve Inventory - FAILS)
|
Backward Rollback: C1 (Cancel Order) <--- C2 (Refund Card) <----------+
Klassifizierung von Saga-Transaktionen
Um einen robusten Saga-Workflow zu entwerfen, muss jeder Schritt in der Transaktionssequenz in einen von drei Strukturtypen kategorisiert werden:
| Transaktionstyp | Beschreibung | Idempotenz- und Rollback-Anforderung |
|---|---|---|
| Vergütungspflichtige Transaktionen | Schritte, die vor dem Punkt ausgeführt wurden, an dem es kein Zurück mehr gibt. Sie können rückgängig gemacht oder rückgängig gemacht werden, wenn ein nachgelagerter Schritt fehlschlägt. | Muss über eine entsprechende Kompensationstransaktion ($C_i$) verfügen. |
| Pivot-Transaktion | Der entscheidende Schritt in der Saga. Wenn der Pivot gelingt, ist die Saga garantiert zu Ende. Wenn dies fehlschlägt, wird die Saga zurückgesetzt. | Weder entschädigungsfähig noch rückgängig zu machen; Es markiert die Grenze zwischen Rollback und Forward Completion. |
| Wiederholbare Transaktionen | Schritte, die nach der Pivot-Transaktion ausgeführt werden. Der Erfolg ist garantiert und es ist keine Entschädigung erforderlich. | Muss streng idempotent sein, da sie automatisch wiederholt werden, bis sie erfolgreich sind. |
Beispielaufschlüsselung für den E-Commerce-Checkout
Stellen Sie sich einen E-Commerce-Bestellvorgang vor, der aus vier Schritten besteht:
- $T_1$: Ausstehende Bestellung erstellen (kompensierbar) $\to$ Rückgängig machen über $C_1$: Bestellung stornieren.
- $T_2$: Zahlung genehmigen (kompensierbar) $\to$ Rückgängig machen über $C_2$: Zahlung zurückerstatten.
- $T_3$: Lagerbestand reservieren (Pivot-Transaktion) $\to$ Wenn die Lagerzuteilung erfolgreich ist, ist die Bestellung abgeschlossen. Wenn dies fehlschlägt, lösen Sie $C_2$ und $C_1$ aus.
- $T_4$: Versandanforderung versenden (Wiederholbar) $\to$ Wird nach dem Pivot ausgeführt; bis zur Lieferung erneut versucht.
Saga-Architekturstile: Choreografie vs. Orchestrierung
Es gibt zwei primäre Architekturstile für die Implementierung des Saga-Musters in verteilten Systemen: Choreografie (dezentralisiert) und Orchestrierung (zentralisiert).
Stil 1: Choreografie (ereignisgesteuerte Dezentralisierung)
In einer choreografiebasierten Saga gibt es keinen zentralen Controller oder Koordinator. Stattdessen kommunizieren Microservices asynchron, indem sie Domänenereignisse abhören, die auf einem zentralen Ereignisbus (wie Apache Kafka, NATS oder RabbitMQ) veröffentlicht werden.
Workflow-Mechaniken
- Order Service führt $T_1$ aus (erstellt eine ausstehende Bestellung) und sendet ein
OrderCreated-Ereignis an Kafka. - Zahlungsdienst hört auf
OrderCreated, führt $T_2$ aus (Kreditkarte belasten) und gibt einPaymentCompleted-Ereignis (oderPaymentFailed) aus. - Inventory Service hört
PaymentCompletedab und führt $T_3$ aus (reserviert Artikel). Wenn Artikel nicht vorrätig sind, wirdInventoryReservationFailedausgegeben. - Zahlungsdienst hört
InventoryReservationFailedab und führt $C_2$ aus (gibt eine Rückerstattung aus). - Bestelldienst hört auf
PaymentRefundedund führt $C_1$ aus (markiert die Bestellung als storniert).
Vorteile der Choreografie
- Loose Kopplung: Dienste abonnieren nur Ereignisthemen; Sie wissen nichts über andere Service-Implementierungen.
- Hoher Durchsatz und Dezentralisierung: Direct Event Pub/Sub eliminiert Engpässe im zentralen Orchestrator.
- Einfachheit für kurze Arbeitsabläufe: Einfache Einrichtung für einfache Arbeitsabläufe mit 2–3 Schritten.
Nachteile der Choreografie
- Risiko zyklischer Abhängigkeit: Dienste können am Ende gegenseitig auf die Ereignisse hören und so komplexe zyklische Abhängigkeiten erzeugen.
- Schwierige Code-Verfolgbarkeit: Um den End-to-End-Geschäftsprozess zu verstehen, muss die Logik über mehrere Codebasis-Repositorys hinweg verfolgt werden.
- Event Storming und Komplexität: Wenn die Anzahl der Schritte zunimmt (z. B. 10+ Dienste), führt die Verwaltung von Fehlerrandfällen zu einer Explosion des Ereignisstatus.
Stil 2: Orchestrierung (zentrales Workflow-Management)
In einer Orchestrierungsbasierten Saga steuert ein dedizierter Mikrodienst namens Saga Orchestrator den gesamten Lebenszyklus der verteilten Transaktion. Der Orchestrator fungiert als zentraler Koordinator, der explizite Befehle an die teilnehmenden Microservices erteilt und deren Antwortereignisse abhört.
Workflow-Mechaniken
- Der Kunde sendet eine Bestellanfrage an den Saga Orchestrator.
- Orchestrator sendet einen
CreateOrder-Befehl an Order Service. Der Bestellservice gibtOrderCreatedzurück. - Orchestrator aktualisiert seine Zustandsmaschine und sendet einen
ProcessPayment-Befehl an Payment Service. Der Zahlungsdienst gibtPaymentSuccessfulzurück. - Orchestrator sendet einen
ReserveInventory-Befehl an Inventory Service. Der Inventardienst gibtInventoryFailed (Out of Stock)zurück. - Der Orchestrator erkennt einen Fehler und leitet den Kompensationsfluss ein:
– Sendet den Befehl
RefundPaymentan Zahlungsdienst. – Sendet den BefehlCancelOrderan Bestellservice. - Orchestrator markiert die Saga-Ausführung als
FAILED.
Vorteile der Orchestrierung
- Zentralisierte Geschäftslogik: Workflow-Status und Geschäftslogik sind in einem einzigen Orchestrator-Dienst oder einer einzigen Zustandsmaschine lokalisiert.
- Keine zyklischen Abhängigkeiten: Microservices reagieren auf Befehle vom Orchestrator; Sie sind nicht von anderen nachgelagerten Diensten abhängig oder wissen nichts davon.
- Klare Überwachung und Fehlerbehebung: Der End-to-End-Transaktionsstatus wird explizit im Statusspeicher des Orchestrators gespeichert (z. B. PostgreSQL oder Workflow-Engines wie Temporal/Camunda).
- Einfachere Fehlerbehandlung: Das Hinzufügen neuer Schritte oder das Ändern von Rollback-Regeln wird vollständig im Orchestrator verwaltet.
Nachteile der Orchestrierung
- Orchestrator-Komplexität: Es besteht die Gefahr, dass zu viel Domänenlogik in den Orchestrator eingefügt wird, was ihn in ein Anti-Pattern „intelligenter Orchestrator, dummer Dienst“ verwandelt.
- Potenzieller Single Point of Failure: Der Orchestrator muss hochverfügbar und zustandsbehaftet sein.
Vergleichsmatrix: Choreografie vs. Orchestrierung
| Funktion | Choreographie | Orchestrierung |
|---|---|---|
| Kontrollstruktur | Dezentral (Event Pub/Sub) | Zentralisiert (Saga-Koordinator / Zustandsmaschine) |
| Kupplung | Extrem niedrig (Dienste verbrauchen Ereignisse) | Mittel (Dienste akzeptieren Befehle vom Koordinator) |
| Prozesssichtbarkeit | Niedrig (auf Protokolldateien verteilt) | Hoch (Einzelstatusspeicher visualisiert den Workflow) |
| Am besten geeignet für | Einfache Arbeitsabläufe (2 bis 4 Serviceschritte) | Komplexe Unternehmensworkflows (5+ Schritte, Verzweigungslogik) |
| Tools / Frameworks | Kafka, RabbitMQ, NATS, AWS EventBridge | Temporal.io, AWS Step Functions, Camunda, Axon |
Isolationsherausforderungen und Gegenmaßnahmen (Umgang mit „ACID minus I“)
Da lokale Transaktionen in einer Saga sofort an ihre lokalen Datenbanken übertragen werden, fehlt dem Saga-Muster die Isolation (I) gegenüber herkömmlichen ACID-Garantien.
Wenn ein Client eine durch $T_1$ geänderte Datenbankzeile liest, während die Saga noch läuft, liest er nicht festgeschrieben, Zwischenstatus. Wenn ein nachgelagerter Schritt fehlschlägt und eine Kompensation auslöst ($C_1$), hat der Client einen Dirty Read durchgeführt.
Häufige Anomalien, die durch mangelnde Isolation verursacht werden
- Verlorene Updates: Saga A aktualisiert einen Datensatz. Bevor Saga A abgeschlossen ist, überschreibt Saga B denselben Datensatz. Wenn Saga A fehlschlägt und eine Kompensation ausführt, überschreibt es das Update von Saga B.
- Dirty Reads: Ein Kunde liest den von Saga A aktualisierten verfügbaren Bestand ($T_1$). Saga A schlägt stromabwärts fehl ($T_3$) und stellt den Lagerbestand wieder her ($C_1$), aber der Kunde hat aufgrund veralteter Daten bereits eine Bestellung aufgegeben.
- Nicht wiederholbare Lesevorgänge: Ein Dienst liest Daten in Schritt $T_1$ und liest sie erneut in Schritt $T_3$, aber eine andere gleichzeitige Saga hat die Daten dazwischen geändert.
Gegenmaßnahmen und Schadensbegrenzungsstrategien
Um die Datenintegrität trotz fehlender Isolation aufrechtzuerhalten, implementieren Softwarearchitekten spezifische Isolationsentwurfsmuster:
1. Semantische Sperre (Ausstehender/gekennzeichneter Status)
Wenn die lokale Transaktion $T_1$ einen Datenbankeintrag aktualisiert, setzt sie ein Statusfeld auf PENDING oder APPROVAL_REQUIRED (z. B. ORDER_PENDING_PAYMENT).
Andere gleichzeitige Sagas, die diesen Datensatz lesen, müssen das Flag für die semantische Sperre überprüfen und ihr Verhalten blockieren oder ändern, bis sich der Status in COMMITTED oder CANCELLED ändert.
2. Auftragsbestätigung
Gestalten Sie die Abfolge lokaler Transaktionen so, dass risikoreiche oder irreversible Vorgänge spät in der Saga-Ausführung erfolgen, wodurch das Schwachstellenfenster minimiert wird.
3. Validierung erneut lesen (optimistische Parallelitätskontrolle)
Bevor Sie einen kritischen Schritt oder eine Kompensation ausführen, lesen Sie den Datensatz der Zieldatenbank erneut und überprüfen Sie die Versionszeitstempel (version_id), um sicherzustellen, dass keine gleichzeitigen Änderungen vorgenommen wurden.
4. Pessimistische Sichtweise
Ordnen Sie die Schritte einer Saga neu an, um das wirtschaftliche Risiko zu minimieren (z. B. platzieren Sie die Zahlungsautorisierung so nah wie möglich an der Pivot-Transaktion).
Produktionssequenzfluss: Kompensationstransaktionen
Nachfolgend finden Sie das vollständige Ablaufdiagramm, das einen Vorwärtsausführungsfehler und die daraus resultierende Rückwärtskompensationsausführung veranschaulicht:
Praktische Code-Implementierungen
Sehen wir uns produktionsreife Implementierungsbeispiele für Choreographie (in Go) und Orchestrierung (in Java Spring Boot) an.
Implementierung 1: Choreografiebasierte Saga in Go
In diesem Go-Beispiel demonstrieren wir einen Bestelldienst, der die Auftragserstellung abwickelt und über einen Ereignisbroker auf Zahlungsfehlerereignisse wartet, um eine kompensierende Rollback-Logik auszuführen.
package saga
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
)
// Event definitions
type OrderCreatedEvent struct {
OrderID string `json:"order_id"`
CustomerID string `json:"customer_id"`
Amount float64 `json:"amount"`
}
type PaymentFailedEvent struct {
OrderID string `json:"order_id"`
Reason string `json:"reason"`
}
// OrderRepository handles local DB operations
type OrderRepository interface {
CreateOrder(ctx context.Context, orderID string, amount float64) error
UpdateOrderStatus(ctx context.Context, orderID string, status string) error
}
// EventBus abstraction for message broker (e.g., Kafka / NATS)
type EventBus interface {
Publish(topic string, payload []byte) error
Subscribe(topic string, handler func(payload []byte)) error
}
type OrderSagaChoreographer struct {
repo OrderRepository
eventBus EventBus
}
func NewOrderSagaChoreographer(repo OrderRepository, bus EventBus) *OrderSagaChoreographer {
c := &OrderSagaChoreographer{repo: repo, eventBus: bus}
c.registerSubscriptions()
return c
}
// Step 1: Forward Transaction (T1)
func (s *OrderSagaChoreographer) StartOrderSaga(ctx context.Context, orderID, customerID string, amount float64) error {
// Execute local database transaction
err := s.repo.CreateOrder(ctx, orderID, amount)
if err != nil {
return fmt.Errorf("failed local DB transaction T1: %w", err)
}
// Emit domain event for downstream Payment Service
event := OrderCreatedEvent{OrderID: orderID, CustomerID: customerID, Amount: amount}
bytes, _ := json.Marshal(event)
log.Printf("[SAGA][T1] Order %s created. Publishing OrderCreatedEvent...", orderID)
return s.eventBus.Publish("orders.created", bytes)
}
// Register subscription for compensating events
func (s *OrderSagaChoreographer) registerSubscriptions() {
_ = s.eventBus.Subscribe("payments.failed", func(payload []byte) {
var event PaymentFailedEvent
if err := json.Unmarshal(payload, &event); err != nil {
log.Printf("[ERROR] Corrupt payment event: %v", err)
return
}
// Execute Compensating Transaction (C1)
s.handlePaymentFailed(context.Background(), event)
})
}
// Step C1: Compensating Transaction
func (s *OrderSagaChoreographer) handlePaymentFailed(ctx context.Context, event PaymentFailedEvent) {
log.Printf("[SAGA][C1] Payment failed for Order %s (Reason: %s). Rolling back local order...", event.OrderID, event.Reason)
// Revert order status to CANCELLED in local DB
err := s.repo.UpdateOrderStatus(ctx, event.OrderID, "CANCELLED_PAYMENT_FAILED")
if err != nil {
log.Printf("[CRITICAL] Failed to execute compensating transaction C1 for Order %s: %v", event.OrderID, err)
// Trigger alert or write to Dead Letter Queue (DLQ)
return
}
log.Printf("[SAGA][SUCCESS] Order %s successfully compensated and cancelled.", event.OrderID)
}
Implementierung 2: Orchestrierungsbasierte Saga in Java (Spring Boot)
In diesem Java-Beispiel erstellen wir einen Saga Orchestrator mithilfe eines Zustandsmaschinenmusters, um Weiterleitungsbefehle zu koordinieren und kompensierende Rollbacks auszuführen, wenn ein Downstream-Schritt fehlschlägt.
package com.ghaznix.saga.orchestrator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.UUID;
public enum SagaState {
STARTED,
ORDER_CREATED,
PAYMENT_PROCESSED,
INVENTORY_RESERVED,
COMPLETED,
COMPENSATING_PAYMENT,
COMPENSATING_ORDER,
FAILED
}
@Service
public class OrderSagaOrchestrator {
private static final Logger log = LoggerFactory.getLogger(OrderSagaOrchestrator.class);
private final OrderServiceClient orderClient;
private final PaymentServiceClient paymentClient;
private final InventoryServiceClient inventoryClient;
public OrderSagaOrchestrator(OrderServiceClient orderClient,
PaymentServiceClient paymentClient,
InventoryServiceClient inventoryClient) {
this.orderClient = orderClient;
this.paymentClient = paymentClient;
this.inventoryClient = inventoryClient;
}
public boolean executeOrderSaga(String customerId, String productId, double amount, int quantity) {
String sagaId = UUID.randomUUID().toString();
log.info("[SAGA {}] Starting Order Saga Workflow...", sagaId);
SagaState currentState = SagaState.STARTED;
String orderId = null;
String paymentId = null;
try {
// Step 1: Forward Local Tx T1 - Create Order
log.info("[SAGA {}][T1] Sending CreateOrder command...", sagaId);
orderId = orderClient.createOrder(customerId, productId, amount);
currentState = SagaState.ORDER_CREATED;
// Step 2: Forward Local Tx T2 - Process Payment
log.info("[SAGA {}][T2] Sending ProcessPayment command for Order {}...", sagaId, orderId);
paymentId = paymentClient.chargePayment(orderId, customerId, amount);
currentState = SagaState.PAYMENT_PROCESSED;
// Step 3: Forward Local Tx T3 (Pivot Step) - Reserve Inventory
log.info("[SAGA {}][T3] Sending ReserveInventory command for Product {}...", sagaId, productId);
boolean stockReserved = inventoryClient.reserveStock(productId, quantity);
if (!stockReserved) {
throw new InventoryAllocationException("Stock allocation failed: Item out of stock.");
}
currentState = SagaState.INVENTORY_RESERVED;
log.info("[SAGA {}][SUCCESS] Saga completed successfully!", sagaId);
return true;
} catch (Exception ex) {
log.error("[SAGA {}][FAILURE] Step failed during state {}. Triggering Compensation...", sagaId, currentState, ex);
rollbackSaga(sagaId, currentState, orderId, paymentId);
return false;
}
}
private void rollbackSaga(String sagaId, SagaState failedState, String orderId, String paymentId) {
log.info("[SAGA {}] Initiating backward compensating transactions from state: {}", sagaId, failedState);
// Compensate Step 2 if payment was processed
if (failedState == SagaState.PAYMENT_PROCESSED || failedState == SagaState.INVENTORY_RESERVED) {
try {
log.info("[SAGA {}][C2] Executing Payment Refund compensation for Payment {}...", sagaId, paymentId);
paymentClient.refundPayment(paymentId);
} catch (Exception e) {
log.error("[CRITICAL][SAGA {}] Payment refund C2 failed! Manual intervention or DLQ required.", sagaId, e);
}
}
// Compensate Step 1 if order was created
if (failedState != SagaState.STARTED && orderId != null) {
try {
log.info("[SAGA {}][C1] Executing Order Cancel compensation for Order {}...", sagaId, orderId);
orderClient.cancelOrder(orderId);
} catch (Exception e) {
log.error("[CRITICAL][SAGA {}] Order cancellation C1 failed!", sagaId, e);
}
}
log.info("[SAGA {}] Saga compensation workflow finished. Final State: FAILED.", sagaId);
}
}
Produktionsgrundlagen: Saga mit dem transaktionalen Outbox-Muster kombinieren
Sowohl in der Choreografie als auch in der Orchestrierung erfordert die Ausführung einer lokalen Transaktion ($T_i$) die Veröffentlichung eines Domänenereignisses oder -befehls über das Netzwerk.
Wenn Ihr Dienst seine SQL-Datenbank aktualisiert und dann eine Nachricht an Kafka veröffentlicht, führt ein Netzwerkfehler nach dem Datenbank-Commit zum stillen Ereignisverlust. Umgekehrt führt die Veröffentlichung der Nachricht vor dem Datenbank-Commit zur Verarbeitung von Phantomereignissen.
Um dieses Problem zu lösen, müssen Sagas mit dem Transactional Outbox Pattern gepaart werden:
[Service Database Transaction Boundary]
+-----------------------------------------------------+
| 1. INSERT INTO business_table (orders/payments) |
| 2. INSERT INTO outbox_table (event_payload) |
+-----------------------------------------------------+
|
(CDC / Polling Message Relay)
|
v
[Message Broker / Kafka]
Durch die Beibehaltung der Ereignisnutzlast in einer outbox-Tabelle innerhalb der gleichen lokalen Datenbanktransaktion wird die Atomizität gewährleistet. Ein asynchroner Hintergrundprozess (wie Debezium oder ein Polling-Relay) liest aus der Postausgangstabelle und veröffentlicht Ereignisse zuverlässig an Kafka.
Darüber hinaus muss jeder Downstream-Dienstkonsument Idempotenz implementieren (unter Verwendung eindeutiger idempotency_key- oder Nachrichtendeduplizierungsheader), damit doppelte Nachrichtenzustellungen bei Wiederholungsversuchen keine doppelten Gebühren oder Bestandszuweisungen auslösen.
Checkliste für architektonische Entscheidungen
Nutzen Sie diese praktische Entscheidungsmatrix beim Entwerfen verteilter Transaktionen für Microservice-Anwendungen:
Do you need cross-service data consistency?
|
+----------------+----------------+
| No | Yes
v v
Standard Single Service Can you accept Eventual
Local Database Consistency (BASE)?
|
+----------------+----------------+
| No | Yes
v v
Use Monolithic Core Adopt Saga Pattern
with Single ACID DB |
|
How complex is the workflow?
|
+-----------------+-----------------+
| Simple (2-3 steps) | Complex (4+ steps/branches)
v v
Choreography Saga Orchestration Saga
(Event-Driven Bus) (Temporal/Custom State Machine)
Fazit und wichtige Erkenntnisse
Das Saga-Muster ist ein wesentliches Architekturmuster für die Verwaltung verteilter Transaktionen über Microservice-Grenzen hinweg, ohne Ressourcen zu sperren oder die Systemverfügbarkeit zu beeinträchtigen.
Zusammenfassung der Checkliste:
- Verzicht auf 2PC/XA in Cloud-nativen Microservices: Zwei-Phasen-Commit führt zu enger Sperrung, hoher Latenz und schwerwiegenden Verfügbarkeitsengpässen.
- Transaktionen in lokale Schritte aufteilen: Teilen Sie globale Operationen in lokale Transaktionen ($T_1 \dots T_n$) auf, gepaart mit umkehrenden kompensierenden Transaktionen ($C_1 \dots C_{n-1}$).
- Wählen Sie den richtigen Architekturstil:
- Verwenden Sie Choreografie für einfache, 2–3-stufige ereignisgesteuerte Abläufe mit loser Kopplung.
- Verwenden Sie Orchestrierung für komplexe Geschäftsabläufe, die eine zentralisierte Sichtbarkeit, Verzweigung und Zustandsmaschinenverfolgung erfordern.
- Implementieren Sie Isolations-Gegenmaßnahmen: Schützen Sie sich vor fehlerhaften Lesevorgängen und verlorenen Aktualisierungen durch die Verwendung von semantischen Sperren (
PENDING-Flags) und Re-Read Optimistic Locking. - Zuverlässiges Messaging garantieren: Koppeln Sie Saga-Implementierungen immer mit dem Transaktions-Postausgangsmuster und erzwingen Sie Idempotente Verbraucher, um Wiederholungsversuche sicher zu verarbeiten.
Durch die durchdachte Implementierung des Saga-Musters können Sie hochverfügbare, skalierbare Microservices erstellen, die auch bei Netzwerkausfällen stabil und konsistent bleiben.