Saga-patroon bij gedistribueerde transacties - Met voorbeelden in Go
Transacties in Microservices met het Saga-patroon
Het Saga-patroon biedt een elegante oplossing door gedistribueerde transacties op te splitsen in een reeks lokale transacties met compenserende acties.
In plaats van te vertrouwen op gedistribueerde vergrendelingen die operaties tussen services kunnen blokkeren, stelt Saga uiteindelijke consistentie mogelijk door middel van een sequentie van omkeerbare stappen. Dit maakt het ideaal voor langlopende bedrijfsprocessen.
In microservices-architecturen is het behouden van dataconsistentie tussen services een van de grootste uitdagingen. Traditionele ACID-transacties werken niet wanneer operaties meerdere services met onafhankelijke databases omvatten, waardoor ontwikkelaars alternatieve benaderingen moeten zoeken om data-integriteit te waarborgen.
Deze handleiding demonstreert de implementatie van het Saga-patroon in Go met praktische voorbeelden, zowel voor orchestration (orchestratie) als choreography (choreografie). Als u een snelle referentie nodig heeft voor Go-fundamenten, biedt het Go Cheat Sheet een nuttig overzicht.
Deze mooie afbeelding is gegenereerd door AI model Flux 1 dev.
Het Saga-patroon begrijpen
Het Saga-patroon is oorspronkelijk beschreven door Hector Garcia-Molina en Kenneth Salem in 1987. In de context van microservices is het een sequentie van lokale transacties waarbij elke transactie data binnen een enkele service bijwerkt. Als een stap faalt, worden compenserende transacties uitgevoerd om de effecten van voorgaande stappen ongedaan te maken.
In tegenstelling tot traditionele gedistribueerde transacties die Two-Phase Commit (2PC) gebruiken, houdt Saga geen vergrendelingen vast over services heen, waardoor het geschikt is voor langlopende bedrijfsprocessen. De afweging is uiteindelijke consistentie in plaats van sterke consistentie.
Belangrijkste kenmerken
- Geen gedistribueerde vergrendelingen: Elke service beheert zijn eigen lokale transactie
- Compenserende acties: Elke operatie heeft een corresponderende rollback-mechanisme
- Uiteindelijke consistentie: Het systeem bereikt uiteindelijk een consistente staat
- Langlopend: Geschikt voor processen die seconden, minuten of zelfs uren duren
Implementatiebenaderingen voor Saga
Er zijn twee primaire benaderingen voor het implementeren van het Saga-patroon: orchestratie en choreografie.
Orchestratie-patroon
Bij orchestratie beheert een centrale coördinator (orchestrator) de volledige transactiestroom. De orchestrator is verantwoordelijk voor:
- Het aanroepen van services in de juiste volgorde
- Het afhandelen van fouten en het triggeren van compensaties
- Het bijhouden van de status van de saga
- Het coördineren van opnieuw pogingen en time-outs
Voordelen:
- Centraliseerde controle en zichtbaarheid
- Makkelijker te begrijpen en te debuggen
- Betere foutafhandeling en herstel
- Eenvoudiger testen van de algehele stroom
Nadelen:
- Enig punt van falen (hoewel dit kan worden gemitigeerd)
- Aanvullende service om te onderhouden
- Kan een bottleneck worden voor complexe stromen
Voorbeeld in Go:
type OrderSagaOrchestrator struct {
orderService OrderService
paymentService PaymentService
inventoryService InventoryService
shippingService ShippingService
}
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
sagaID := generateSagaID()
// Stap 1: Order aanmaken
orderID, err := o.orderService.Create(order)
if err != nil {
return err
}
// Stap 2: Voorraad reserveren
if err := o.inventoryService.Reserve(order.Items); err != nil {
o.orderService.Cancel(orderID) // Compenseren
return err
}
// Stap 3: Betaling verwerken
paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
if err != nil {
o.inventoryService.Release(order.Items) // Compenseren
o.orderService.Cancel(orderID) // Compenseren
return err
}
// Stap 4: Verzending aanmaken
if err := o.shippingService.CreateShipment(orderID); err != nil {
o.paymentService.Refund(paymentID) // Compenseren
o.inventoryService.Release(order.Items) // Compenseren
o.orderService.Cancel(orderID) // Compenseren
return err
}
return nil
}
Choreografie-patroon
Bij choreografie is er geen centrale coördinator. Elke service weet wat ze moet doen en communiceert via gebeurtenissen. Services luisteren naar gebeurtenissen en reageren dienovereenkomstig. Deze gebeurtenisgestuurde benadering is bijzonder krachtig wanneer gecombineerd met message streaming platforms zoals AWS Kinesis, die schaalbare infrastructuur bieden voor gebeurtenisverdeling tussen microservices. Zie voor een uitgebreide handleiding over het implementeren van gebeurtenisgestuurde microservices met Kinesis Building Event-Driven Microservices with AWS Kinesis.
Voordelen:
- Gedeconcentreerd en schaalbaar
- Geen enkel punt van falen
- Services blijven losjes gekoppeld
- Natuurlijke fit voor gebeurtenisgestuurde architecturen
Nadelen:
- Moeilijker om de algehele stroom te begrijpen
- Moeilijk te debuggen en traceren
- Complexe foutafhandeling
- Risico op cyclische afhankelijkheden
Voorbeeld met Gebeurtenisgestuurde Architectuur:
// 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
}
// Opmerking: s.repo.Save gevolgd door s.eventBus.Publish is een dual-write.
// Vervang dit in productie door het transactionele outbox-patroon zodat de
// gebeurtenis atomisch wordt geschreven met de order-regel en door een relay wordt gepubliceerd.
func (s *OrderService) HandlePaymentFailed(event PaymentFailedEvent) error {
return s.repo.Cancel(event.OrderID) // Compensatie
}
// 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 {
// Compensatie: betaling terugstorten
return s.client.Refund(event.PaymentID)
}
Compensatiestrategieën
Compensatie is de kern van het Saga-patroon. Elke operatie moet een corresponderende compensatie hebben die de effecten kan omkeren.
Types van compensatie
-
Omkeerbare operaties: Operaties die direct ongedaan kunnen worden gemaakt
- Voorbeeld: Gereserveerde voorraad vrijgeven, betalingen terugstorten
-
Compenserende acties: Andere operaties die het omgekeerde effect bereiken
- Voorbeeld: Een order annuleren in plaats van deze te verwijderen
-
Pessimistische compensatie: Resources vooraf toewijzen die vrijgegeven kunnen worden
- Voorbeeld: Voorraad reserveren voordat de betaling wordt verwerkt
-
Optimistische compensatie: Operaties uitvoeren en compenseren indien nodig
- Voorbeeld: Eerst betalen, terugstorten als voorraad niet beschikbaar is
Vereisten voor idempotentie
Alle operaties en compensaties moeten idempotent zijn. Dit zorgt ervoor dat het opnieuw proberen van een mislukte operatie geen dubbele effecten veroorzaakt. Even belangrijk is het ervoor zorgen dat elke saga-deelnemer zijn gebeurtenissen betrouwbaar publiceert na een lokale commit — het transactionele outbox-patroon is de standaard manier om die kloof tussen een database-schrijfactie en een broker-publicatie te dichten.
func (s *PaymentService) Refund(paymentID string) error {
// Controleren of al teruggestort
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // Al teruggestort, idempotent
}
// Restitutie verwerken
return s.processRefund(paymentID)
}
Best Practices
1. Saga-statusbeheer
Behoud de status van elke saga-instantie om voortgang te volgen en herstel mogelijk te maken. Bij het persistent maken van saga-status naar een database is het kiezen van de juiste ORM cruciaal voor prestaties en onderhoudbaarheid. Voor PostgreSQL-implementaties, overweeg de vergelijking in Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc om de beste optie te selecteren voor uw saga-statusopslagbehoeften:
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. Time-outafhandeling
Implementeer time-outs voor elke stap om te voorkomen dat sagas onbepaald blijven hangen:
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():
// Time-out opgetreden, compenseren
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. Logica voor opnieuw proberen
Implementeer exponentiële back-off voor tijdelijke fouten:
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 voor Saga-status
Gebruik event sourcing om een volledige audit trail bij te houden. Bij het implementeren van event stores en replay-mechanismen kunnen Go-generics helpen bij het creëren van typeveilige, herbruikbare code voor gebeurtenisafhandeling. Zie voor geavanceerde patronen met generics in Go 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 en Observabiliteit
Implementeer uitgebreide logging en 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
}
Veelvoorkomende patronen en anti-patronen
Patronen om te volgen
- Saga Coordinator Pattern: Gebruik een speciale service voor orchestratie
- Outbox Pattern: Zorg voor betrouwbare gebeurtenispublicatie
- Idempotency Keys: Gebruik unieke keys voor alle operaties
- Saga State Machine: Modelleer saga als een statemachine
Anti-patronen om te vermijden
- Synchrone compensatie: Wacht niet tot compensatie is voltooid
- Geneste sagas: Vermijd sagas die andere sagas aanroepen (gebruik sub-sagas in plaats daarvan)
- Gedeelde status: Deel geen status tussen sagastappen
- Langlopende stappen: Splijt stappen op die te lang duren
Tools en Frameworks
Verschillende frameworks kunnen helpen bij het implementeren van Saga-patronen:
- Temporal: Workflow-orchestratieplatform met ingebouwde Saga-ondersteuning
- Zeebe: Workflow-engine voor microservices-orchestratie
- Eventuate Tram: Saga-framework voor Spring Boot
- AWS Step Functions: Serverless workflow-orchestratie
- Apache Camel: Integratieframework met Saga-ondersteuning
Voor orchestrator-services die CLI-interfaces nodig hebben voor beheer en monitoring, biedt Building CLI Applications in Go with Cobra & Viper uitstekende patronen voor het maken van commandoregeltools om te interageren met saga-orchestrators.
Bij het implementeren van op saga-gebaseerde microservices in Kubernetes kan een service mesh de observabiliteit, beveiliging en trafficbeheer aanzienlijk verbeteren. Implementing Service Mesh with Istio and Linkerd behandelt hoe service meshes gedistribueerde transactiepatronen aanvullen door cross-cutting concerns te bieden zoals gedistribueerde tracing en circuit breaking.
Wanneer het Saga-patroon te gebruiken
Gebruik het Saga-patroon wanneer:
- ✅ Operaties meerdere microservices overspannen
- ✅ Langlopende bedrijfsprocessen
- ✅ Uiteindelijke consistentie acceptabel is
- ✅ U gedistribueerde vergrendelingen wilt vermijden
- ✅ Services onafhankelijke databases hebben
Vermijd wanneer:
- ❌ Sterke consistentie vereist is
- ❌ Operaties eenvoudig en snel zijn
- ❌ Alle services dezelfde database delen
- ❌ Compensatielogica te complex is
Conclusie
Het Saga-patroon is essentieel voor het beheren van gedistribueerde transacties in microservices-architecturen. Hoewel het complexiteit introduceert, biedt het een praktische oplossing voor het behouden van dataconsistentie over servic grenzen heen. Kies voor orchestratie voor betere controle en zichtbaarheid, of voor choreografie voor schaalbaarheid en losse koppeling. Zorg er altijd voor dat operaties idempotent zijn, implementeer juiste compensatielogica en behoud uitgebreide observabiliteit.
De sleutel tot succesvolle Saga-implementatie is het begrijpen van uw consistentievereisten, het zorgvuldig ontwerpen van compensatielogica en het kiezen van de juiste benadering voor uw gebruikscase. Met de juiste implementatie stelt Saga u in staat om veerkrachtige, schaalbare microservices te bouwen die data-integriteit behouden over gedistribueerde systemen.
Nuttige 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