Saga patroon in gedistribueerde transacties - Met voorbeelden in Go
Transacties in Microservices met het Saga patroon
De Saga patroon biedt een elegante oplossing door gedistribueerde transacties te verdelen in een reeks lokale transacties met compenserende acties.
In plaats van te vertrouwen op gedistribueerde vergrendelingen die bewerkingen over diensten kunnen blokkeren, maakt Saga uiteindelijk consistentie mogelijk via een reeks omkeerbare stappen, waardoor het ideaal is voor lange lopende zakelijke processen.
In microservices-architecturen is het behouden van gegevensconsistentie over diensten een van de meest uitdagende problemen. Traditionele ACID-transacties werken niet wanneer bewerkingen meerdere diensten met onafhankelijke databases betreffen, waardoor ontwikkelaars op zoek zijn naar alternatieve aanpakken om gegevensintegriteit te waarborgen.
Deze gids demonstreert de implementatie van het Saga patroon in Go met praktische voorbeelden die zowel de orchestratie- als de choreografiebenadering behandelen. Als je een snelle verwijzing nodig hebt voor Go-fundamenten, biedt de Go Cheat Sheet een nuttige overzicht.
Deze mooie afbeelding is gegenereerd door AI model Flux 1 dev.
Het begrijpen van het Saga patroon
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 gegevens binnen één dienst bijwerkt. Als een stap mislukt, worden compenserende transacties uitgevoerd om de effecten van vorige stappen ongedaan te maken.
In tegenstelling tot traditionele gedistribueerde transacties die gebruikmaken van tweefasencommit (2PC), houdt Saga geen vergrendelingen over diensten, waardoor het geschikt is voor lange lopende zakelijke processen. Het compromis is uiteindelijke consistentie in plaats van sterke consistentie.
Belangrijke kenmerken
- Geen gedistribueerde vergrendelingen: Elke dienst beheert zijn eigen lokale transactie
- Compenserende acties: Elke bewerking heeft een overeenkomstige terugdraai-mechanisme
- Uiteindelijke consistentie: Het systeem bereikt uiteindelijk een consistente toestand
- Lange lopende: Geschikt voor processen die seconden, minuten of zelfs uren duren
Implementatiebenaderingen van het Saga patroon
Er zijn twee primaire benaderingen voor het implementeren van het Saga patroon: orchestratie en choreografie.
Orchestratiepatroon
Bij orchestratie beheert een centrale coördinator (orchestrator) de hele transactiestroom. De orchestrator is verantwoordelijk voor:
- Het aanroepen van diensten in de juiste volgorde
- Het afhandelen van fouten en het activeren van compensaties
- Het behouden van de staat van de saga
- Het coördineren van herproeven en time-outs
Voordelen:
- Centrale controle en zichtbaarheid
- Eenvoudiger te begrijpen en debuggen
- Betere foutafhandeling en herstel
- Eenvoudiger testen van de gehele stroom
Nadelen:
- Enkel punt van falen (hoewel dit kan worden geminderd)
- Extra dienst om te onderhouden
- Kan een knelpunt 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 dienst weet wat hij moet doen en communiceert via gebeurtenissen. Diensten luisteren naar gebeurtenissen en reageren daarop. Deze gebeurtenisgestuurde aanpak is vooral 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 gebeurtenisgestuurde microservices met Kinesis, zie Het bouwen van gebeurtenisgestuurde microservices met AWS Kinesis.
Voordelen:
- Decentraliseerd en schaalbaar
- Geen enkel punt van falen
- Diensten blijven los gekoppeld
- Natuurlijke fit voor gebeurtenisgestuurde architecturen
Nadelen:
- Moeilijker om de gehele stroom te begrijpen
- Moeilijk om te debuggen en te 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
}
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 terugbetalen
return s.client.Refund(event.PaymentID)
}
Compensatiestrategieën
Compensatie is het hart van het Saga patroon. Elke bewerking moet een overeenkomstige compensatie hebben die de effecten ervan kan ongedaan maken.
Typen compensatie
-
Omkeerbare bewerkingen: Bewerkingen die direct ongedaan kunnen worden gemaakt
- Voorbeeld: Voorraad vrijgeven, betalingen terugbetalen
-
Compenserende acties: Verschillende bewerkingen die het omgekeerde effect bereiken
- Voorbeeld: Een bestelling annuleren in plaats van te verwijderen
-
Pessimistische compensatie: Vooraf toewijzen van middelen die vrijgegeven kunnen worden
- Voorbeeld: Voorraad reserveren voordat betaling wordt verwerkt
-
Optimistische compensatie: Bewerkingen uitvoeren en compenseren indien nodig
- Voorbeeld: Betaling eerst verwerken, terugbetalen indien voorraad niet beschikbaar is
Idempotentie vereisten
Alle bewerkingen en compensaties moeten idempotent zijn. Dit zorgt ervoor dat het herproberen van een gefaalde bewerking geen dubbele effecten veroorzaakt.
func (s *PaymentService) Refund(paymentID string) error {
// Controleer of al terugbetaald
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // Al terugbetaald, idempotent
}
// Verwerk terugbetaling
return s.processRefund(paymentID)
}
Beste praktijken
1. Saga-staatbeheer
Houd de staat van elk Saga-exemplaar bij om de voortgang te volgen en herstel mogelijk te maken. Bij het opslaan van Saga-staat in een database is het kiezen van de juiste ORM cruciaal voor prestaties en onderhoudbaarheid. Voor implementaties op basis van PostgreSQL, overweeg dan de vergelijking in Vergelijking van Go ORMs voor PostgreSQL: GORM vs Ent vs Bun vs sqlc om de beste keuze te maken voor je Saga-staatopslagbehoeften:
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-outbehandeling
Implementeer time-outs voor elke stap om te voorkomen dat Sagas oneindig lang 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 is opgetreden, compenseer
if err := step.Compensate(); err != nil {
return fmt.Errorf("compensatie is mislukt: %w", err)
}
return fmt.Errorf("stap %s is na %v time-out gegaan", step.Name(), o.timeout)
}
}
3. Herproeflogica
Implementeer exponentiële backoff 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("bewerking is mislukt na %d herproeven", maxRetries)
}
4. Event sourcing voor Saga-staat
Gebruik event sourcing om een volledige audittrail te behouden. Bij het implementeren van eventopslag en herplaatsingsmechanismen kunnen Go-generieken helpen bij het maken van typesafe, herbruikbare eventverwerkingscode. Voor geavanceerde patronen met generieken in Go, zie Go-generieken: Gebruikscases en patronen.
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("maken van payload is mislukt: %w", err)
}
version, err := s.store.GetNextVersion(sagaID)
if err != nil {
return fmt.Errorf("maken van versie is mislukt: %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("maken van gebeurtenissen is mislukt: %w", err)
}
saga := NewSaga()
for _, event := range events {
if err := saga.Apply(event); err != nil {
return nil, fmt.Errorf("toepassen van gebeurtenis is mislukt: %w", err)
}
}
return saga, nil
}
5. Monitoring en observabiliteit
Implementeer uitgebreide logboekregistratie 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 is gestart")
// ... saga uitvoering
return nil
}
Algemene patronen en anti-patronen
Patronen om te volgen
- Saga-coördinatorpatroon: Gebruik een toegewezen dienst voor orchestratie
- Outboxpatroon: Zorg voor betrouwbare gebeurtenisuitzending
- Idempotente sleutels: Gebruik unieke sleutels voor alle bewerkingen
- Saga-staatmachine: Model de saga als een staatmachine
Anti-patronen om te vermijden
- Synchronische compensatie: Wacht niet op het voltooien van compensatie
- Geneste Sagas: Vermijd dat Sagas andere Sagas aanroepen (gebruik sub-Sagas in plaats daarvan)
- Gedeelde staat: Deel geen staat tussen Saga-stappen
- Lange lopende stappen: Breng stappen die te lang duren op in kleinere stappen
Tools en frameworks
Verschillende frameworks kunnen helpen bij het implementeren van het Saga patroon:
- Temporal: Workflow-orchestratieplatform met ingebouwde Saga-ondersteuning
- Zeebe: Workflow-engine voor microservices-orchestratie
- Eventuate Tram: Saga-framework voor Spring Boot
- AWS Step Functions: Serverloze workflow-orchestratie
- Apache Camel: Integratieframework met Saga-ondersteuning
Voor orchestrator-diensten die CLI-interfaces nodig hebben voor beheer en monitoring, biedt Het bouwen van CLI-toepassingen in Go met Cobra & Viper uitstekende patronen voor het maken van command-line-tools om te interageren met Saga-orchestrators.
Bij het implementeren van Saga-gebaseerde microservices in Kubernetes, kan het implementeren van een service mesh aanzienlijk de observabiliteit, beveiliging en verkeerbeheer verbeteren. Het implementeren van een service mesh met Istio en Linkerd bespreekt hoe service meshes gedistribueerde transactiepatronen aanvullen door cross-cutting zorgen zoals gedistribueerde tracing en circuit breaking te bieden.
Wanneer het Saga patroon te gebruiken is
Gebruik het Saga patroon wanneer:
- ✅ Bewerkingen meerdere microservices betreffen
- ✅ Lange lopende zakelijke processen
- ✅ Uiteindelijke consistentie is aanvaardbaar
- ✅ Je wilt vermijden dat gedistribueerde vergrendelingen worden gebruikt
- ✅ Diensten onafhankelijke databases hebben
Vermijd wanneer:
- ❌ Sterke consistentie vereist is
- ❌ Bewerkingen eenvoudig en snel zijn
- ❌ Alle diensten 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 gegevensconsistentie over servicegrenzen. Kies voor orchestratie voor betere controle en zichtbaarheid, of voor choreografie voor schaalbaarheid en los gekoppeldheid. Zorg altijd dat bewerkingen idempotent zijn, implementeer correcte compensatielogica en behoud uitgebreide observabiliteit.
Het sleutel tot een succesvolle Saga-implementatie is het begrijpen van je consistentie-eisen, zorgvuldig ontwerpen van compensatielogica en het kiezen van de juiste aanpak voor je gebruikscase. Met een correcte implementatie maakt Saga het mogelijk om resiliënte, schaalbare microservices te bouwen die gegevensintegriteit behouden in gedistribueerde systemen.
Nuttige links
- Microservices Patterns door Chris Richardson
- Saga patroon - Martin Fowler
- Eventuate Tram Saga Framework
- Temporal Workflow Engine
- AWS Step Functions Documentatie
- Go Cheat Sheet
- Go-generieken: Gebruikscases en patronen
- Vergelijking van Go ORMs voor PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Het bouwen van CLI-toepassingen in Go met Cobra & Viper
- Het implementeren van een service mesh met Istio en Linkerd
- Het bouwen van gebeurtenisgestuurde microservices met AWS Kinesis