Saga-Pattern bei 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 lokaler Transaktionen mit kompensierenden Aktionen aufteilt.
Anstatt sich auf verteilte Sperren zu verlassen, die Vorgänge über Dienste hinweg blockieren können, ermöglicht Saga durch eine Sequenz umkehrbarer Schritte eine finale Konsistenz (eventual consistency). Dies macht es ideal für lang laufende Geschäftsprozesse.
In Microservice-Architekturen ist die Aufrechterhaltung der Datenkonsistenz über Dienste hinweg eines der größten Herausforderungen. Traditionelle ACID-Transaktionen funktionieren nicht, wenn Vorgänge mehrere Dienste mit unabhängigen Datenbanken umfassen, was Entwickler dazu zwingt, nach alternativen Ansätzen zur Sicherstellung der Datenintegrität zu suchen.
Dieser Leitfaden 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 einen hilfreichen Überblick.
Dieses hübsche Bild wurde von AI model Flux 1 dev generiert.
Das Saga-Muster verstehen
Das Saga-Muster wurde ursprünglich 1987 von Hector Garcia-Molina und Kenneth Salem beschrieben. Im Kontext von Microservices ist es eine Sequenz lokaler Transaktionen, bei der jede Transaktion Daten innerhalb eines einzelnen Dienstes aktualisiert. Wenn ein Schritt fehlschlägt, werden kompensierende Transaktionen ausgeführt, um die Auswirkungen der vorherigen Schritte rückgängig zu machen.
Im Gegensatz zu traditionellen verteilten Transaktionen, die das Two-Phase-Commit-Protokoll (2PC) verwenden, hält Saga keine Sperren über Dienste hinweg. Dies macht es geeignet für lang laufende Geschäftsprozesse. Der Kompromiss ist finale Konsistenz statt starker Konsistenz.
Hauptmerkmale
- Keine verteilten Sperren: Jeder Dienst verwaltet seine eigene lokale Transaktion
- Kompensierende Aktionen: Jede Operation hat einen entsprechenden Rollback-Mechanismus
- Finale Konsistenz: Das System erreicht schließlich einen konsistenten Zustand
- Lang laufend: Geeignet für Prozesse, die Sekunden, Minuten oder sogar Stunden dauern
Implementierungsansätze für Saga
Es gibt zwei primäre Ansä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:
- Aufrufen von Diensten in der richtigen Reihenfolge
- Behandlung von Fehlern und Auslösen von Kompensationen
- Verwalten des Zustands der Saga
- Koordinieren von Wiederholungsversuchen und Timeouts
Vorteile:
- Zentrale Steuerung und Übersicht
- Einfacher zu verstehen und zu debuggen
- Bessere Fehlerbehandlung und Wiederherstellung
- Einfacheres Testen des gesamten Flusses
Nachteile:
- Single Point of Failure (obwohl dies gemildert werden kann)
- Zusätzlicher Dienst zur Wartung
- Kann bei komplexen Flüssen zu einem Engpass 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: Inventar reservieren
if err := o.inventoryService.Reserve(order.Items); err != nil {
o.orderService.Cancel(orderID) // Kompensieren
return err
}
// Schritt 3: Zahlung verarbeiten
paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
if err != nil {
o.inventoryService.Release(order.Items) // Kompensieren
o.orderService.Cancel(orderID) // Kompensieren
return err
}
// Schritt 4: Versand erstellen
if err := o.shippingService.CreateShipment(orderID); err != nil {
o.paymentService.Refund(paymentID) // Kompensieren
o.inventoryService.Release(order.Items) // Kompensieren
o.orderService.Cancel(orderID) // Kompensieren
return err
}
return nil
}
Choreografiemuster
Bei der Choreografie gibt es keinen zentralen Koordinator. Jeder Dienst weiß, was zu tun ist, und kommuniziert über Events. Dienste hören auf Events und reagieren entsprechend. Dieser ereignisgesteuerte Ansatz ist besonders leistungsfähig, wenn er mit Message-Streaming-Plattformen wie AWS Kinesis kombiniert wird, die skalierbare Infrastruktur für die Event-Verteilung über Microservices hinweg bereitstellen. Für einen umfassenden Leitfaden zur Implementierung ereignisgesteuerter Microservices mit Kinesis sehen Sie Building Event-Driven Microservices with AWS Kinesis.
Vorteile:
- Dezentral und skalierbar
- Kein Single Point of Failure
- Dienste bleiben lose gekoppelt
- Natürliche Passform für ereignisgesteuerte Architekturen
Nachteile:
- Schwerer, den Gesamtfluss zu verstehen
- Schwer 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) // Kompensation
}
// 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 {
// Kompensation: Zahlung erstatten
return s.client.Refund(event.PaymentID)
}
Kompensationsstrategien
Kompensation ist das Herzstück des Saga-Musters. Jede Operation muss eine entsprechende Kompensation haben, die ihre Auswirkungen umkehren kann.
Arten der Kompensation
-
Umkehrbare Operationen: Operationen, die direkt rückgängig gemacht werden können
- Beispiel: Freigabe reservierten Inventars, Rückerstattung von Zahlungen
-
Kompensierende Aktionen: Andere Operationen, die den umgekehrten Effekt erzielen
- Beispiel: Stornieren einer Bestellung anstatt sie zu löschen
-
Pessimistische Kompensation: Vorab Zuweisung von Ressourcen, die freigegeben werden können
- Beispiel: Reservierung des Inventars vor der Belastung der Zahlung
-
Optimistische Kompensation: Ausführen von Operationen und Kompensieren bei Bedarf
- Beispiel: Zahlung zuerst belasten, erstatten, wenn Inventar nicht verfügbar ist
Anforderungen an die Idempotenz
Alle Operationen und Kompensationen müssen idempotent sein. Dies stellt sicher, dass das Wiederholen einer fehlgeschlagenen Operation keine doppelten Effekte verursacht.
func (s *PaymentService) Refund(paymentID string) error {
// Prüfen, ob bereits erstattet
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // Bereits erstattet, idempotent
}
// Rückerstattung verarbeiten
return s.processRefund(paymentID)
}
Best Practices
1. Saga-Zustandsverwaltung
Verwalten Sie den Zustand jeder Saga-Instanz, um den Fortschritt zu verfolgen und Wiederherstellung zu ermöglichen. Bei der Persistierung des Saga-Zustands in einer Datenbank ist die Wahl des richtigen ORMs entscheidend für Leistung und Wartbarkeit. Für PostgreSQL-basierte Implementierungen sollten Sie die_comparison in Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc berücksichtigen, um die beste Option für Ihre Saga-Zustandsspeicherbedürfnisse 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 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("compensation failed: %w", err)
}
return fmt.Errorf("step %s timed out after %v", step.Name(), o.timeout)
}
}
3. Retry-Logik
Implementieren Sie exponentiellen 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 failed after %d retries", maxRetries)
}
4. Event Sourcing für den Saga-Zustand
Verwenden Sie Event Sourcing, um einen vollständigen Audit-Trail aufrechtzuerhalten. Bei der Implementierung von Event Stores und Replay-Mechanismen können Go-Generics helfen, typsicheren, wiederverwendbaren Event-Handling-Code zu erstellen. Für fortgeschrittene Muster mit Generics in Go sehen Sie Go Generics: Use Cases and Patterns.
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("failed to marshal payload: %w", err)
}
version, err := s.store.GetNextVersion(sagaID)
if err != nil {
return fmt.Errorf("failed to get 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("failed to get events: %w", err)
}
saga := NewSaga()
for _, event := range events {
if err := saga.Apply(event); err != nil {
return nil, fmt.Errorf("failed to apply event: %w", err)
}
}
return saga, nil
}
5. Monitoring und Observability
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 started")
// ... saga execution
return nil
}
Häufige Muster und Anti-Patterns
Zu befolgende Muster
- Saga Coordinator Pattern: Verwenden Sie einen dedizierten Dienst für die Orchestrierung
- Outbox Pattern: Stellen Sie eine zuverlässige Event-Veröffentlichung sicher
- Idempotency Keys: Verwenden Sie eindeutige Schlüssel für alle Operationen
- Saga State Machine: Modellieren Sie die Saga als Zustandsmaschine
Zu vermeidende Anti-Patterns
- Synchronous Compensation: Warten Sie nicht auf die Fertigstellung der Kompensation
- Nested Sagas: Vermeiden Sie Sagas, die andere Sagas aufrufen (verwenden Sie stattdessen Sub-Sagas)
- Shared State: Teilen Sie keinen Zustand zwischen Saga-Schritten
- Long-Running Steps: Unterteilen Sie Schritte, die zu lange dauern
Tools und Frameworks
Mehrere Frameworks können bei der Implementierung von Saga-Mustern helfen:
- Temporal: Workflow-Orchestrierungsplattform mit eingebauter Saga-Unterstützung
- Zeebe: Workflow-Engine für Microservice-Orchestrierung
- 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 Monitoring benötigen, bietet Building CLI Applications in Go with Cobra & Viper exzellente Muster zur Erstellung von Befehlszeilentools zur Interaktion mit Saga-Orchestratoren.
Bei der Bereitstellung saga-basierter Microservices in Kubernetes kann die Implementierung eines Service Mesh die Observability, Sicherheit und Traffic-Verwaltung erheblich verbessern. Implementing Service Mesh with Istio and Linkerd beschreibt, wie Service Meshes verteilte Transaktionsmuster ergänzen, indem sie Cross-Cutting Concerns wie verteiltes Tracing und Circuit Breaking bereitstellen.
Wann man das Saga-Muster verwenden sollte
Verwenden Sie das Saga-Muster, wenn:
- ✅ Operationen mehrere Microservices umfassen
- ✅ Lang laufende Geschäftsprozesse
- ✅ Finale Konsistenz akzeptabel ist
- ✅ Sie verteilte Sperren vermeiden 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 entscheidend für die Verwaltung verteilter Transaktionen in Microservice-Architekturen. Obwohl es Komplexität einführt, bietet es eine praktische Lösung zur Aufrechterhaltung der Datenkonsistenz über Dienstgrenzen hinweg. Wählen Sie Orchestrierung für bessere Kontrolle und Übersicht oder Choreografie für Skalierbarkeit und lose Kopplung. Stellen Sie immer sicher, dass Operationen idempotent sind, implementieren Sie eine ordnungsgemäße Kompensationslogik und halten Sie eine umfassende Observability aufrecht.
Der Schlüssel zur erfolgreichen Saga-Implementierung liegt im Verständnis Ihrer Konsistenzanforderungen, der sorgfältigen Gestaltung der Kompensationslogik und der Wahl des richtigen Ansatzes für Ihren Anwendungsfall. Mit einer ordnungsgemäßen Implementierung ermöglicht Saga Ihnen, resiliente, skalierbare Microservices zu bauen, die die Datenintegrität über verteilte Systeme hinweg aufrechterhalten.
Nützliche Links
- Microservices Patterns by Chris Richardson
- Saga Pattern - Martin Fowler
- Eventuate Tram Saga Framework
- Temporal Workflow Engine
- AWS Step Functions Documentation
- Go Cheat Sheet
- Go Generics: Use Cases and Patterns
- Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Implementing CQRS in Go
- Building CLI Applications in Go with Cobra & Viper
- Implementing Service Mesh with Istio and Linkerd
- Building Event-Driven Microservices with AWS Kinesis