Het 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, maakt Saga uiteindelijke consistentie mogelijk via een reeks omkeerbare stappen. Dit maakt het ideaal voor langlopende bedrijfsprocessen.
In microservice-architecturen is het handhaven van dataconsistentie tussen services een van de meest uitdagende problemen. Traditionele ACID-transacties werken niet wanneer operaties meerdere services met onafhankelijke databases overspannen, waardoor ontwikkelaars op zoek moeten naar alternatieve benaderingen om data-integriteit te waarborgen.
Deze gids demonstreert de implementatie van het Saga-patroon in Go met praktische voorbeelden die zowel orkestratie- als choreografiebenaderingen dekken. 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 werd oorspronkelijk beschreven door Hector Garcia-Molina en Kenneth Salem in 1987. In de context van microservices is het een reeks lokale transacties waarbij elke transactie data bijwerkt binnen een enkele service. Als een stap faalt, worden compenserende transacties uitgevoerd om de effecten van voorgaande stappen ongedaan te maken.
In tegenstelling tot traditionele gedistribueerde transacties die twee-fasen commit (2PC) gebruiken, houdt Saga geen vergrendelingen vast tussen services, 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 bijbehorende 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: orkestratie en choreografie.
Orkestratiepatroon
Bij orkestratie beheert een centrale coördinator (orchestrator) de volledige transactiestroom. De orchestrator is verantwoordelijk voor:
- Services aanroepen in de juiste volgorde
- Het afhandelen van fouten en het activeren van compensaties
- Het bijhouden van de status van de saga
- Het coördineren van retries en time-outs
Voordelen:
- Gecentraliseerde controle en zichtbaarheid
- Easier te begrijpen en te debuggen
- Betere foutafhandeling en herstel
- Eenvoudiger testen van de algehele stroom
Nadelen:
- Enig falingspunt (hoewel dit kan worden gemitigeerd)
- Aanvullende service die moet worden 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: Bestelling maken
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 maken
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
}
Choreografiepatroon
Bij choreografie is er geen centrale coördinator. Elke service weet wat het moet doen en communiceert via gebeurtenissen. Services luisteren naar gebeurtenissen en reageren dienovereenkomstig. Deze gebeurtenisgedreven aanpak is bijzonder krachtig wanneer gecombineerd met message streaming-platforms zoals AWS Kinesis, die schaalbare infrastructuur bieden voor gebeurtenisverdeling over microservices. Voor een uitgebreide gids over het implementeren van gebeurtenisgedreven microservices met Kinesis, zie Building Event-Driven Microservices with AWS Kinesis.
Voordelen:
- Gedecentraliseerd en schaalbaar
- Geen enkel falingspunt
- Services blijven losjes gekoppeld
- Natuurlijke fit voor gebeurtenisgedreven architecturen
Nadelen:
- Moeilijker om de algehele stroom te begrijpen
- Moeilijk te debuggen en traceren
- Complexe foutafhandeling
- Risico op cyclische afhankelijkheden
Voorbeeld met Gebeurtenisgedreven 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
}
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 het hart van het Saga-patroon. Elke operatie moet een bijbehorende compensatie hebben die de effecten kan omkeren.
Typen 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 bestelling annuleren in plaats van deze te verwijderen
-
Pessimistische compensatie: Resources vooraf toewijzen die kunnen worden vrijgegeven
- Voorbeeld: Voorraad reserveren voordat de betaling wordt afgeschreven
-
Optimistische compensatie: Operaties uitvoeren en compenseren indien nodig
- Voorbeeld: Eerst betaling afschrijven, 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.
func (s *PaymentService) Refund(paymentID string) error {
// Controleren of al terugbetaald
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // Al terugbetaald, idempotent
}
// Terugbetaling verwerken
return s.processRefund(paymentID)
}
Best Practices
1. Saga-statusbeheer
Behoud de status van elk saga-instantie om voortgang te volgen en herstel mogelijk te maken. Bij het aanhouden van saga-status in een database is het kiezen van de juiste ORM cruciaal voor prestaties en onderhoudbaarheid. Voor PostgreSQL-gebaseerde implementaties, overweeg de vergelijking in Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc om de beste fit te selecteren voor uw behoeften op het gebied van saga-statusopslag:
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 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. Retry-logica
Implementeer exponentiële back-off voor voorbijgaande 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 volledig audittrail bij te houden. Bij het implementeren van event stores en replay-mechanismen kunnen Go-generics helpen bij het maken van typeveilige, herbruikbare code voor gebeurtenisafhandeling. Voor geavanceerde patronen met generics in Go, zie 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 Observability
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 Coördinator Patroon: Gebruik een dedicated service voor orkestratie
- Outbox Patroon: Zorg voor betrouwbare gebeurtenispublicatie
- Idempotentie Sleutels: Gebruik unieke sleutels voor alle operaties
- Saga Statemachine: Modelleer saga als een statemachine
Anti-patronen om te vermijden
- Synchrone Compensatie: Wacht niet op de voltooiing van compensatie
- Geneste Sagas: Vermijd sagas die andere sagas aanroepen (gebruik sub-sagas in plaats daarvan)
- Gedeelde Status: Deel geen status tussen saga-stappen
- Langlopende Stappen: Breek stappen af die te lang duren
Tools en Frameworks
Verschillende frameworks kunnen helpen bij het implementeren van Saga-patronen:
- Temporal: Workflow-orkestratieplatform met ingebouwde Saga-ondersteuning
- Zeebe: Workflow-engine voor microservice-orkestratie
- Eventuate Tram: Saga-framework voor Spring Boot
- AWS Step Functions: Serverless workflow-orkestratie
- Apache Camel: Integratie-framework 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 service meshes in Kubernetes voor saga-gebaseerde microservices kan dit de observability, beveiliging en traffic management aanzienlijk verbeteren. Implementing Service Mesh with Istio and Linkerd behandelt hoe service meshes gedistribueerde transactiepatronen aanvullen door cross-cutting concerns zoals distributed tracing en circuit breaking te bieden.
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 microservice-architecturen. Hoewel het complexiteit introduceert, biedt het een praktische oplossing voor het handhaven van dataconsistentie over servicegrenzen heen. Kies voor orkestratie voor betere controle en zichtbaarheid, of voor choreografie voor schaalbaarheid en losse koppeling. Zorg er altijd voor dat operaties idempotent zijn, implementeer passende compensatielogica en handhaaf uitgebreide observability.
De sleutel tot een succesvolle Saga-implementatie is het begrijpen van uw consistentievereisten, het zorgvuldig ontwerpen van compensatielogica en het kiezen van de juiste aanpak voor uw use case. Met een 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