Saga-Muster in verteilten Transaktionen - Mit Beispielen in Go
Transaktionen in Microservices mit dem Saga-Muster
Das Saga-Muster bietet eine elegante Lösung, indem es verteilte Transaktionen in eine Reihe von lokalen Transaktionen mit Ausgleichsaktionen aufteilt.
Statt auf verteilte Sperren zu setzen, die Operationen über Dienste blockieren können, ermöglicht Saga eine schlussendliche Konsistenz durch eine Abfolge von rückgängig machbaren Schritten, was es ideal für langlaufende Geschäftsprozesse macht.
In Mikrodienstarchitekturen ist die Aufrechterhaltung der Datenkonsistenz über Dienste hinweg eine der größten Herausforderungen. Traditionelle ACID-Transaktionen funktionieren nicht, wenn Operationen mehrere Dienste mit unabhängigen Datenbanken umspannen, was Entwickler nach alternativen Ansätzen zur Sicherstellung der Datenintegrität suchen lässt.
Diese Anleitung demonstriert die Implementierung des Saga-Musters in Go mit praktischen Beispielen, die sowohl Orchestrierungs- als auch Choreografie-Ansätze abdecken. Wenn Sie eine schnelle Referenz für Go-Grundlagen benötigen, bietet das Go Cheat Sheet eine hilfreiche Übersicht.
Dieses schöne Bild wurde von AI-Modell Flux 1 dev erzeugt.
Verständnis des Saga-Musters
Das Saga-Muster wurde ursprünglich 1987 von Hector Garcia-Molina und Kenneth Salem beschrieben. Im Kontext von Mikrodiensten handelt es sich um eine Abfolge von lokalen Transaktionen, bei denen jede Transaktion Daten innerhalb eines einzelnen Dienstes aktualisiert. Falls ein Schritt fehlschlägt, werden Ausgleichstransaktionen ausgeführt, um die Auswirkungen der vorherigen Schritte rückgängig zu machen.
Im Gegensatz zu traditionellen verteilten Transaktionen, die das Zwei-Phasen-Commit (2PC) verwenden, hält Saga keine Sperren über Dienste hinweg, was es für langlaufende Geschäftsprozesse geeignet macht. Der Kompromiss ist eine schlussendliche Konsistenz anstelle einer starken Konsistenz.
Wichtige Merkmale
- Keine verteilten Sperren: Jeder Dienst verwaltet seine eigene lokale Transaktion
- Ausgleichsaktionen: Jede Operation hat einen entsprechenden Rückgängig-Mechanismus
- Schlussendliche Konsistenz: Das System erreicht irgendwann einen konsistenten Zustand
- Langlaufend: Geeignet für Prozesse, die Sekunden, Minuten oder sogar Stunden dauern
Implementierungsansätze für Sagas
Es gibt zwei Hauptansätze zur Implementierung des Saga-Musters: Orchestrierung und Choreografie.
Orchestrierungsmuster
Bei der Orchestrierung verwaltet ein zentraler Koordinator (Orchestrator) den gesamten Transaktionsfluss. Der Orchestrator ist verantwortlich für:
- Aufruf der Dienste in der richtigen Reihenfolge
- Behandlung von Fehlern und Auslösung von Ausgleichsaktionen
- Beibehaltung des Zustands der Saga
- Koordination von Wiederholungen und Timeouts
Vorteile:
- Zentralisierte Kontrolle und Sichtbarkeit
- Einfacher zu verstehen und zu debuggen
- Bessere Fehlerbehandlung und Wiederherstellung
- Einfachere Tests des gesamten Ablaufs
Nachteile:
- Einziger Ausfallpunkt (obwohl dies abgemildert werden kann)
- Zusätzlicher Dienst, der gewartet werden muss
- Kann für komplexe Abläufe zu einer Flaschenhals werden
Beispiel in Go:
type OrderSagaOrchestrator struct {
orderService OrderService
paymentService PaymentService
inventoryService InventoryService
shippingService ShippingService
}
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
sagaID := generateSagaID()
// Schritt 1: Bestellung erstellen
orderID, err := o.orderService.Create(order)
if err != nil {
return err
}
// Schritt 2: Lagerbestand reservieren
if err := o.inventoryService.Reserve(order.Items); err != nil {
o.orderService.Cancel(orderID) // Ausgleich
return err
}
// Schritt 3: Zahlung bearbeiten
paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
if err != nil {
o.inventoryService.Release(order.Items) // Ausgleich
o.orderService.Cancel(orderID) // Ausgleich
return err
}
// Schritt 4: Versand erstellen
if err := o.shippingService.CreateShipment(orderID); err != nil {
o.paymentService.Refund(paymentID) // Ausgleich
o.inventoryService.Release(order.Items) // Ausgleich
o.orderService.Cancel(orderID) // Ausgleich
return err
}
return nil
}
Choreografie-Muster
Bei der Choreografie gibt es keinen zentralen Koordinator. Jeder Dienst weiß, was zu tun ist, und kommuniziert über Ereignisse. Dienste hören auf Ereignisse und reagieren entsprechend. Dieser ereignisgesteuerte Ansatz ist besonders leistungsfähig, wenn er mit Message-Streaming-Plattformen wie AWS Kinesis kombiniert wird, die eine skalierbare Infrastruktur für die Ereignisverteilung über Mikrodienste hinweg bieten. Für eine umfassende Anleitung zur Implementierung ereignisgesteuerter Mikrodienste mit Kinesis siehe Building Event-Driven Microservices with AWS Kinesis.
Vorteile:
- Dezentralisiert und skalierbar
- Kein einziger Ausfallpunkt
- Dienste bleiben locker gekoppelt
- Natürliche Passform für ereignisgesteuerte Architekturen
Nachteile:
- Schwerer zu verstehen, wie der gesamte Ablauf funktioniert
- Schwierig zu debuggen und zu verfolgen
- Komplexe Fehlerbehandlung
- Risiko zyklischer Abhängigkeiten
Beispiel mit ereignisgesteuerter Architektur:
// Order Service
type OrderService struct {
eventBus EventBus
repo OrderRepository
}
func (s *OrderService) CreateOrder(order Order) (string, error) {
orderID, err := s.repo.Save(order)
if err != nil {
return "", err
}
s.eventBus.Publish("OrderCreated", OrderCreatedEvent{
OrderID: orderID,
CustomerID: order.CustomerID,
Items: order.Items,
Total: order.Total,
})
return orderID, nil
}
func (s *OrderService) HandlePaymentFailed(event PaymentFailedEvent) error {
return s.repo.Cancel(event.OrderID) // Ausgleich
}
// Payment Service
type PaymentService struct {
eventBus EventBus
client PaymentClient
}
func (s *PaymentService) HandleOrderCreated(event OrderCreatedEvent) {
paymentID, err := s.client.Charge(event.CustomerID, event.Total)
if err != nil {
s.eventBus.Publish("PaymentFailed", PaymentFailedEvent{
OrderID: event.OrderID,
})
return
}
s.eventBus.Publish("PaymentSucceeded", PaymentSucceededEvent{
OrderID: event.OrderID,
PaymentID: paymentID,
})
}
func (s *PaymentService) HandleInventoryReservationFailed(event InventoryReservationFailedEvent) error {
// Ausgleich: Zahlung zurückerstatten
return s.client.Refund(event.PaymentID)
}
Ausgleichsstrategien
Der Ausgleich ist das Herzstück des Saga-Musters. Jede Operation muss eine entsprechende Ausgleichsaktion haben, die ihre Auswirkungen rückgängig machen kann.
Arten von Ausgleichsaktionen
-
Rückgängig machbare Operationen: Operationen, die direkt rückgängig gemacht werden können
- Beispiel: Freigabe des reservierten Lagerbestands, Rückerstattung von Zahlungen
-
Ausgleichsaktionen: Unterschiedliche Operationen, die den umgekehrten Effekt erzielen
- Beispiel: Stornierung einer Bestellung statt Löschung
-
Pessimistische Ausgleichsaktionen: Ressourcen vorab zuweisen, die freigegeben werden können
- Beispiel: Lagerbestand reservieren, bevor die Zahlung belastet wird
-
Optimistische Ausgleichsaktionen: Operationen ausführen und bei Bedarf ausgleichen
- Beispiel: Zahlung zuerst belasten, rückerstatten, wenn Lagerbestand nicht verfügbar ist
Idempotenz-Anforderungen
Alle Operationen und Ausgleichsaktionen müssen idempotent sein. Dies stellt sicher, dass das erneute Ausführen einer fehlgeschlagenen Operation keine doppelten Auswirkungen verursacht.
func (s *PaymentService) Refund(paymentID string) error {
// Überprüfen, ob bereits rückerstattet
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // Bereits rückerstattet, idempotent
}
// Rückerstattung verarbeiten
return s.processRefund(paymentID)
}
Beste Praktiken
1. Saga-Zustandsverwaltung
Verwalten Sie den Zustand jeder Saga-Instanz, um den Fortschritt zu verfolgen und die Wiederherstellung zu ermöglichen. Bei der Persistierung des Saga-Zustands in einer Datenbank ist die Wahl des richtigen ORMs entscheidend für die Leistung und Wartbarkeit. Für PostgreSQL-basierte Implementierungen sollten Sie den Vergleich in Vergleich von Go-ORMs für PostgreSQL: GORM vs Ent vs Bun vs sqlc berücksichtigen, um die beste Lösung für Ihre Saga-Zustands-Speicheranforderungen auszuwählen:
type SagaState struct {
ID string
Status SagaStatus
Steps []SagaStep
CurrentStep int
CreatedAt time.Time
UpdatedAt time.Time
}
type SagaStep struct {
Service string
Operation string
Status StepStatus
Compensated bool
Data map[string]interface{}
}
2. Timeout-Handling
Implementieren Sie Timeouts für jeden Schritt, um zu verhindern, dass Sagas unendlich lange hängen bleiben:
type SagaOrchestrator struct {
timeout time.Duration
}
func (o *SagaOrchestrator) ExecuteWithTimeout(step SagaStep) error {
ctx, cancel := context.WithTimeout(context.Background(), o.timeout)
defer cancel()
done := make(chan error, 1)
go func() {
done <- step.Execute()
}()
select {
case err := <-done:
return err
case <-ctx.Done():
// Timeout aufgetreten, kompensieren
if err := step.Compensate(); err != nil {
return fmt.Errorf("Kompensation fehlgeschlagen: %w", err)
}
return fmt.Errorf("Schritt %s ist nach %v abgelaufen", step.Name(), o.timeout)
}
}
3. Wiederholungslogik
Implementieren Sie exponentielles Backoff für vorübergehende Fehler:
func retryWithBackoff(operation func() error, maxRetries int) error {
backoff := time.Second
for i := 0; i < maxRetries; i++ {
err := operation()
if err == nil {
return nil
}
if !isTransientError(err) {
return err
}
time.Sleep(backoff)
backoff *= 2
}
return fmt.Errorf("Operation fehlgeschlagen nach %d Versuchen", maxRetries)
}
4. Event Sourcing für Saga-Zustand
Verwenden Sie Event Sourcing, um eine vollständige Audit-Trail zu führen. Bei der Implementierung von Event-Stores und Wiedergabemechanismen können Go-Generics helfen, typsicheren, wiederverwendbaren Event-Handling-Code zu erstellen. Für fortgeschrittene Muster mit Generics in Go siehe Go Generics: Anwendungsfälle und Muster.
type SagaEvent struct {
SagaID string
EventType string
Payload []byte
Timestamp time.Time
Version int64
}
type SagaEventStore struct {
store EventRepository
}
func (s *SagaEventStore) AppendEvent(sagaID string, eventType string, payload interface{}) error {
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("Fehler beim Serialisieren des Payloads: %w", err)
}
version, err := s.store.GetNextVersion(sagaID)
if err != nil {
return fmt.Errorf("Fehler beim Abrufen der Version: %w", err)
}
event := SagaEvent{
SagaID: sagaID,
EventType: eventType,
Payload: data,
Timestamp: time.Now(),
Version: version,
}
return s.store.Save(event)
}
func (s *SagaEventStore) ReplaySaga(sagaID string) (*Saga, error) {
events, err := s.store.GetEvents(sagaID)
if err != nil {
return nil, fmt.Errorf("Fehler beim Abrufen der Ereignisse: %w", err)
}
saga := NewSaga()
for _, event := range events {
if err := saga.Apply(event); err != nil {
return nil, fmt.Errorf("Fehler beim Anwenden des Ereignisses: %w", err)
}
}
return saga, nil
}
5. Überwachung und Beobachtbarkeit
Implementieren Sie umfassende Protokollierung und Tracing:
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
span := tracer.StartSpan("saga.create_order")
defer span.Finish()
span.SetTag("saga.id", sagaID)
span.SetTag("order.id", order.ID)
logger.WithFields(log.Fields{
"saga_id": sagaID,
"order_id": order.ID,
"step": "create_order",
}).Info("Saga gestartet")
// ... Saga-Ausführung
return nil
}
Häufige Muster und Anti-Muster
Zu befolgende Muster
- Saga-Koordinator-Muster: Verwenden Sie einen dedizierten Dienst für die Orchestrierung
- Outbox-Muster: Stellen Sie zuverlässiges Event-Publishing sicher
- Idempotenz-Schlüssel: Verwenden Sie eindeutige Schlüssel für alle Operationen
- Saga-Zustandsmaschine: Modellieren Sie die Saga als Zustandsmaschine
Zu vermeidende Anti-Muster
- Synchrones Kompensieren: Warten Sie nicht auf den Abschluss der Kompensation
- Verschachtelte Sagas: Vermeiden Sie Sagas, die andere Sagas aufrufen (verwenden Sie stattdessen Sub-Sagas)
- Gemeinsamer Zustand: Teilen Sie keinen Zustand zwischen Saga-Schritten
- Lange laufende Schritte: Brechen Sie Schritte herunter, die zu lange dauern
Tools und Frameworks
Mehrere Frameworks können bei der Implementierung von Saga-Mustern helfen:
- Temporal: Workflow-Orchestrierungsplattform mit integrierter Saga-Unterstützung
- Zeebe: Workflow-Engine für die Orchestrierung von Mikroservices
- Eventuate Tram: Saga-Framework für Spring Boot
- AWS Step Functions: Serverlose Workflow-Orchestrierung
- Apache Camel: Integrationsframework mit Saga-Unterstützung
Für Orchestrator-Dienste, die CLI-Schnittstellen für Verwaltung und Überwachung benötigen, bietet Erstellung von CLI-Anwendungen in Go mit Cobra & Viper hervorragende Muster zur Erstellung von Command-Line-Tools zur Interaktion mit Saga-Orchestriern.
Bei der Bereitstellung von saga-basierten Mikroservices in Kubernetes kann die Implementierung eines Service Mesh die Beobachtbarkeit, Sicherheit und Verkehrsverwaltung erheblich verbessern. Implementierung eines Service Mesh mit Istio und Linkerd behandelt, wie Service Meshes verteilte Transaktionsmuster durch die Bereitstellung von Querschnittsfunktionen wie verteiltem Tracing und Circuit Breaking ergänzen.
Wann das Saga-Muster verwenden
Verwenden Sie das Saga-Muster, wenn:
- ✅ Operationen mehrere Mikroservices umspannen
- ✅ Lange laufende Geschäftsprozesse
- ✅ Eventuelle Konsistenz ist akzeptabel
- ✅ Verteilte Sperren vermieden werden müssen
- ✅ Dienste unabhängige Datenbanken haben
Vermeiden Sie es, wenn:
- ❌ Starke Konsistenz erforderlich ist
- ❌ Operationen einfach und schnell sind
- ❌ Alle Dienste dieselbe Datenbank teilen
- ❌ Die Kompensationslogik zu komplex ist
Fazit
Das Saga-Muster ist essenziell für die Verwaltung verteilter Transaktionen in Mikroservices-Architekturen. Obwohl es Komplexität einfügt, bietet es eine praktische Lösung zur Aufrechterhaltung der Datenkonsistenz über Service-Grenzen hinweg. Wählen Sie Orchestrierung für bessere Kontrolle und Sichtbarkeit oder Choreografie für Skalierbarkeit und lockere Kopplung. Stellen Sie sicher, dass Operationen idempotent sind, implementieren Sie eine angemessene Kompensationslogik und halten Sie eine umfassende Beobachtbarkeit aufrecht.
Der Schlüssel zu einer erfolgreichen Saga-Implementierung ist das Verständnis Ihrer Konsistenzanforderungen, die sorgfältige Gestaltung der Kompensationslogik und die Wahl des richtigen Ansatzes für Ihren Anwendungsfall. Mit einer ordnungsgemäßen Implementierung ermöglicht Saga Ihnen den Aufbau widerstandsfähiger, skalierbarer Mikroservices, die die Datenintegrität in verteilten Systemen aufrechterhalten.
Nützliche Links
- Microservices Patterns von Chris Richardson
- Saga-Muster - Martin Fowler
- Eventuate Tram Saga Framework
- Temporal Workflow Engine
- AWS Step Functions Dokumentation
- Go Cheat Sheet
- Go Generics: Anwendungsfälle und Muster
- Vergleich von Go-ORMs für PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Erstellung von CLI-Anwendungen in Go mit Cobra & Viper
- Implementierung eines Service Mesh mit Istio und Linkerd
- Erstellung ereignisgesteuerter Mikroservices mit AWS Kinesis