Saga-mönstret för distribuerade transaktioner – med exempel i Go
Transaktioner i mikrotjänster med sagamönstret
Mönstret Saga ger en elegant lösning genom att bryta upp distribuerade transaktioner i en serie lokala transaktioner med kompenserande åtgärder.
Istället för att förlita sig på distribuerade lås som kan blockera operationer över flera tjänster, möjliggör Saga slutlig konsistens genom en sekvens av reversibla steg, vilket gör det idealiskt för långvariga affärsprocesser.
I arkitekturer med mikrotjänster är upprätthållande av datakonsistens över tjänster ett av de mest utmanande problemen. Traditionella ACID-transaktioner fungerar inte när operationer sträcker sig över flera tjänster med oberoende databaser, vilket lämnar utvecklare att söka efter alternativa metoder för att säkerställa dataintegritet.
Denna guide demonstrerar implementering av Saga-mönstret i Go med praktiska exempel som täcker både orkestrering och koreografi. Om du behöver en snabb referens för Go-grundläggande, ger Go Cheat Sheet en hjälpsam översikt.
Detta fina bild är genererad av AI-modellen Flux 1 dev.
Förstå Saga-mönstret
Saga-mönstret beskrevs ursprungligen av Hector Garcia-Molina och Kenneth Salem 1987. I kontexten av mikrotjänster är det en sekvens av lokala transaktioner där varje transaktion uppdaterar data inom en enda tjänst. Om något steg misslyckas, körs kompenserande transaktioner för att ånka effekterna av föregående steg.
Till skillnad från traditionella distribuerade transaktioner som använder två-fas-kommitt (2PC), håller Saga inga lås över tjänster, vilket gör det lämpligt för långvariga affärsprocesser. Avvägningen är slutlig konsistens snarare än stark konsistens.
Viktiga egenskaper
- Inga distribuerade lås: Varje tjänst hanterar sin egen lokala transaktion
- Kompenserande åtgärder: Varje operation har en motsvarande rollback-mekanism
- Slutlig konsistens: Systemet når slutligen en konsistent tillstånd
- Långvarig: Lämplig för processer som tar sekunder, minuter eller till och med timmar
Implementeringsmetoder för Saga
Det finns två huvudsakliga metoder för att implementera Saga-mönstret: orkestrering och koreografi.
Orkestreringsmönstret
Vid orkestrering hanterar en central koordinator (orkestrator) hela transaktionsflödet. Orkestratorn är ansvarig för:
- Att anropa tjänster i rätt ordning
- Att hantera fel och utlösa kompensationer
- Att underhålla sagans tillstånd
- Att koordinera försök och tidsgränser
Fördelar:
- Centraliserad kontroll och överskådlighet
- Enklare att förstå och felsöka
- Bättre felhantering och återhämtning
- Enklare testning av det övergripande flödet
Nackdelar:
- En enda felkälla (även om detta kan mildras)
- En extra tjänst att underhålla
- Kan bli en flaskhals för komplexa flöden
Exempel i Go:
type OrderSagaOrchestrator struct {
orderService OrderService
paymentService PaymentService
inventoryService InventoryService
shippingService ShippingService
}
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
sagaID := generateSagaID()
// Steg 1: Skapa order
orderID, err := o.orderService.Create(order)
if err != nil {
return err
}
// Steg 2: Reservera lager
if err := o.inventoryService.Reserve(order.Items); err != nil {
o.orderService.Cancel(orderID) // Kompensera
return err
}
// Steg 3: Bearbeta betalning
paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
if err != nil {
o.inventoryService.Release(order.Items) // Kompensera
o.orderService.Cancel(orderID) // Kompensera
return err
}
// Steg 4: Skapa leverans
if err := o.shippingService.CreateShipment(orderID); err != nil {
o.paymentService.Refund(paymentID) // Kompensera
o.inventoryService.Release(order.Items) // Kompensera
o.orderService.Cancel(orderID) // Kompensera
return err
}
return nil
}
Koreografimönstret
Vid koreografi finns det ingen central koordinator. Varje tjänst vet vad den ska göra och kommunicerar via händelser. Tjänster lyssnar på händelser och reagerar därefter. Denna händelsedrivna metod är särskilt kraftfull när den kombineras med plattformar för händelseströmning som AWS Kinesis, som tillhandahåller skalbar infrastruktur för händelsedistribution över mikrotjänster. För en omfattande guide om att implementera händelsedrivna mikrotjänster med Kinesis, se Bygg händelsedrivna mikrotjänster med AWS Kinesis.
Fördelar:
- Decentraliserad och skalbar
- Inga enskilda felkällor
- Tjänsterna förblir löst kopplade
- Naturlig passform för händelsedrivna arkitekturer
Nackdelar:
- Svårare att förstå det övergripande flödet
- Svårt att felsöka och spåra
- Komplex felhantering
- Risk för cykliska beroenden
Exempel med händelsedrivna arkitekturer:
// 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
}
// Obs: s.repo.Save följt av s.eventBus.Publish är en dubbel-skrivning.
// I produktion, byt ut detta mot det transaktionella outbox-mönstret så att
// händelsen skrivs atomiskt tillsammans med order-raden och publiceras av en relay.
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: återbetal betalning
return s.client.Refund(event.PaymentID)
}
Kompensationsstrategier
Kompensation är hjärtat i Saga-mönstret. Varje operation måste ha en motsvarande kompensation som kan reversera dess effekter.
Typer av kompensation
-
Reversibla operationer: Operationer som kan ångras direkt
- Exempel: Att frigöra reserverat lager, återbetalning av betalningar
-
Kompenserande åtgärder: Olika operationer som uppnår den motsatta effekten
- Exempel: Att avbeställa en order istället för att ta bort den
-
Pessimistisk kompensation: Fördela resurser som kan frigöras
- Exempel: Reservera lager innan betalningen debiteras
-
Optimistisk kompensation: Utför operationer och kompensera vid behov
- Exempel: Debitera betalningen först, återbetal om lagret inte finns tillgängligt
Krav på idempotens
Alla operationer och kompensationer måste vara idempotenta. Detta säkerställer att ett försök att köra en misslyckad operation igen inte orsakar dubbla effekter. Det är lika viktigt att se till att varje saga-deltagare pålitligt publicerar sina händelser efter en lokal commit — det transaktionella outbox-mönstret är det standardiserade sättet att stänga luckan mellan ett databasskrivning och en broker-publicering.
func (s *PaymentService) Refund(paymentID string) error {
// Kontrollera om redan återbetald
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // Redan återbetald, idempotent
}
// Bearbeta återbetalning
return s.processRefund(paymentID)
}
Bästa praxis
1. Hantering av Saga-tillstånd
Underhåll tillståndet för varje saga-instans för att spåra framsteg och möjliggöra återhämtning. När man persistar saga-tillstånd till en databas, är valet av rätt ORM avgörande för prestanda och underhållbarhet. För implementeringar baserade på PostgreSQL, överväg jämförelsen i Jämförelse av Go ORM för PostgreSQL: GORM vs Ent vs Bun vs sqlc för att välja den bästa passformen för dina behov av saga-tillståndslagring:
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. Hantering av tidsgränser
Implementera tidsgränser för varje steg för att förhindra att sagor hänger kvar oändligt:
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():
// Tidsgräns överskriden, kompensera
if err := step.Compensate(); err != nil {
return fmt.Errorf("kompensation misslyckades: %w", err)
}
return fmt.Errorf("steg %s tog för lång tid efter %v", step.Name(), o.timeout)
}
}
3. Logik för försök
Implementera exponentiell backoff för övergående fel:
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("operationen misslyckades efter %d försök", maxRetries)
}
4. Händelsekällning för Saga-tillstånd
Använd händelsekällning för att underhålla en komplett revisionshistorik. När man implementerar händelselager och återuppläsningsmekanismer, kan Go-generika hjälpa till att skapa typsäker, återanvändbar kod för händelsehantering. För avancerade mönster som använder generika i Go, se Go Generics: Användningsområden och mönster.
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("lyckades inte marshala payload: %w", err)
}
version, err := s.store.GetNextVersion(sagaID)
if err != nil {
return fmt.Errorf("lyckades inte hämta 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("lyckades inte hämta händelser: %w", err)
}
saga := NewSaga()
for _, event := range events {
if err := saga.Apply(event); err != nil {
return nil, fmt.Errorf("lyckades inte applicera händelse: %w", err)
}
}
return saga, nil
}
5. Övervakning och observabilitet
Implementera omfattande loggning och spårning:
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 startad")
// ... saga-exekvering
return nil
}
Vanliga mönster och anti-mönster
Mönster att följa
- Saga-koordinatormönstret: Använd en dedikerad tjänst för orkestrering
- Outbox-mönstret: Säkerställ pålitlig publicering av händelser
- Idempotensnycklar: Använd unika nycklar för alla operationer
- Saga-tillståndsmaskin: Modellera saga som en tillståndsmaskin
Anti-mönster att undvika
- Synkron kompensation: Vänta inte på att kompensation ska vara klar
- Nested Sagas: Undvik sagor som anropar andra sagor (använd undersagor istället)
- Delat tillstånd: Dela inte tillstånd mellan saga-steg
- Långvariga steg: Bryt ner steg som tar för lång tid
Verktyg och ramverk
Flera ramverk kan hjälpa till att implementera Saga-mönster:
- Temporal: Plattform för arbetsflödesorkestrering med inbyggt stöd för Saga
- Zeebe: Arbetsflödesmotor för mikrotjänstorkestrering
- Eventuate Tram: Saga-ramverk för Spring Boot
- AWS Step Functions: Serverless arbetsflödesorkestrering
- Apache Camel: Integrationsramverk med stöd för Saga
För orkestreringstjänster som behöver CLI-gränssnitt för hantering och övervakning, Bygg CLI-applikationer i Go med Cobra & Viper ger utmärkta mönster för att skapa kommandotolksverktyg för att interagera med saga-orkestratorer.
När man distribuerar saga-baserade mikrotjänster i Kubernetes, kan implementering av ett service mesh avsevärt förbättra observabilitet, säkerhet och trafikhantering. Implementering av Service Mesh med Istio och Linkerd täcker hur service meshes kompletterar mönster för distribuerade transaktioner genom att tillhandahålla tvärgående bekymmer som distribuerad spårning och circuit breaking.
När man ska använda Saga-mönstret
Använd Saga-mönstret när:
- ✅ Operationer sträcker sig över flera mikrotjänster
- ✅ Långvariga affärsprocesser
- ✅ Slutlig konsistens är acceptabel
- ✅ Du behöver undvika distribuerade lås
- ✅ Tjänsterna har oberoende databaser
Undvik när:
- ❌ Stark konsistens krävs
- ❌ Operationer är enkla och snabba
- ❌ Alla tjänster delar samma databas
- ❌ Kompensationslogiken är för komplex
Slutsats
Saga-mönstret är avgörande för att hantera distribuerade transaktioner i mikrotjänstarkitekturer. Även om det introducerar komplexitet, ger det en praktisk lösning för att upprätthålla datakonsistens över tjänstegränser. Välj orkestrering för bättre kontroll och överskådlighet, eller koreografi för skalbarhet och lös koppling. Säkerställ alltid att operationer är idempotenta, implementera korrekt kompensationslogik och underhåll omfattande observabilitet.
Nyckeln till framgångsrik Saga-implementering är att förstå dina konsistenskrav, noggrant designa kompensationslogiken och välja rätt metod för ditt användningsfall. Med korrekt implementering möjliggör Saga att du bygger motståndskraftiga, skalbara mikrotjänster som upprätthåller dataintegritet över distribuerade system.
Användbara länkar
- Microservices Patterns av Chris Richardson
- Saga Pattern - Martin Fowler
- Eventuate Tram Saga Framework
- Temporal Workflow Engine
- AWS Step Functions Dokumentation
- Go Cheat Sheet
- Go Generics: Användningsområden och mönster
- Jämförelse av Go ORM för PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Implementering av CQRS i Go
- Bygg CLI-applikationer i Go med Cobra & Viper
- Implementering av Service Mesh med Istio och Linkerd
- Bygg händelsedrivna mikrotjänster med AWS Kinesis