Saga-mönstret för distribuerade transaktioner – med exempel i Go
Transaktioner i mikrotjänster med Sagamönstret
Mönstret Saga erbjuder 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 händelsekonsistens (eventual consistency) genom en sekvens av reversibla steg, vilket gör det idealiskt för långvariga affärsprocesser.
I arkitekturer med mikrotjänster är det att upprätthålla datakonsistens över tjänsterna en 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 utvecklarna med att leta efter alternativa metoder för att säkerställa dataintegritet.
Denna guide visar implementation av Saga-mönstret i Go med praktiska exempel som täcker både orkestrering och koreografi-approacher. Om du behöver en snabb referens för Go-grundläggande, ger Go-fuskbladet en hjälpsam översikt.
Detta fina bild är genererad av AI-modellen Flux 1 dev.
Förståelse av Saga-mönstret
Saga-mönstret beskrevs ursprungligen av Hector Garcia-Molina och Kenneth Salem 1987. I sammanhanget 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, utförs kompenserande transaktioner för att ångra effekterna av föregående steg.
I motsats till traditionella distribuerade transaktioner som använder två-fas-commit (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 händelsekonsistens 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
- Händelsekonsistens: Systemet når slutligen ett konsistent tillstånd
- Långvarig: Lämplig för processer som tar sekunder, minuter eller till och med timmar
Implementation av Saga
Det finns två huvudsakliga metoder för att implementera Saga-mönstret: orkestrering och koreografi.
Orkestrering
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 upprätthålla sagans tillstånd
- Att koordinera omförsök och tidsgränser
Fördelar:
- Centraliserad kontroll och överblick
- Enklare att förstå och felsöka
- Bättre felhantering och återhämtning
- Enklare testning av det övergripande flödet
Nackdelar:
- En enda felpunkt (ä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 frakt
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
}
Koreografi
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 approach är särskilt kraftfull när den kombineras med plattformar för händelsestreamning som AWS Kinesis, som tillhandahåller skalbar infrastruktur för händelsedistribution över mikrotjänster. För en omfattande guide om implementation av händelsedrivna mikrotjänster med Kinesis, se Bygg händelsedrivna mikrotjänster med AWS Kinesis.
Fördelar:
- Decentraliserad och skalbar
- Inga enskilda felpunkter
- 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ändelsedragen arkitektur:
// 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: å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: Frigöra reserverat lager, återbetal betalningar
-
Kompenserande åtgärder: Olika operationer som uppnår den motsatta effekten
- Exempel: Avbryt en order istället för att ta bort den
-
Pessimistisk kompensation: Föralloca resurser som kan frigöras
- Exempel: Reservera lager innan betalning belastas
-
Optimistisk kompensation: Utför operationer och kompensera vid behov
- Exempel: Belastning betalning först, återbetal om lagret inte finns
Krav på idempotens
Alla operationer och kompensationer måste vara idempotenta. Detta säkerställer att ett omförsök av en misslyckad operation inte orsakar dubbla effekter.
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
Upprätthå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 PostgreSQL-baserade implementationer, överväg jämförelsen i Jämförelse av Go ORM:er för PostgreSQL: GORM vs Ent vs Bun vs sqlc för att välja den bästa passformen för dina behov av lagring av saga-tillstånd:
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 överskreds, kompensera
if err := step.Compensate(); err != nil {
return fmt.Errorf("kompensation misslyckades: %w", err)
}
return fmt.Errorf("steget %s tog för lång tid efter %v", step.Name(), o.timeout)
}
}
3. Logik för omförsök
Implementera exponentiell backoff för tillfälliga 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 omförsök", maxRetries)
}
4. Händelskällning för Saga-tillstånd
Använd händelskällning (event sourcing) för att upprätthålla en komplett revisionshistorik. När man implementerar händelselagrings och återuppspelingsmekanismer kan Go-generika hjälpa till att skapa typsäker, återanvändbar kod för händelsehantering. För avancerade mönster med 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("misslyckades med att serialisera payload: %w", err)
}
version, err := s.store.GetNextVersion(sagaID)
if err != nil {
return fmt.Errorf("misslyckades med att 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("misslyckades med att hämta händelser: %w", err)
}
saga := NewSaga()
for _, event := range events {
if err := saga.Apply(event); err != nil {
return nil, fmt.Errorf("misslyckades med att 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 antimönster
Mönster att följa
- Saga-koordinatormönster: Använd en dedikerad tjänst för orkestrering
- Outbox-mönster: 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
Antimönster att undvika
- Synkron kompensation: Vänta inte på att kompensation ska slutföras
- Nästa 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 upp 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änst-orkestrering
- 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 mikrotjänster baserade på saga i Kubernetes, kan implementering av en tjänstnätverksmesh (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ärsnittsbekymmer som distribuerad spårning och kretsavbrott.
När ska man använda Saga-mönstret
Använd Saga-mönstret när:
- ✅ Operationer sträcker sig över flera mikrotjänster
- ✅ Långvariga affärsprocesser
- ✅ Händelsekonsistens ä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 överblick, eller koreografi för skalbarhet och lös koppling. Säkerställ alltid att operationer är idempotenta, implementera korrekt kompensationslogik och upprätthåll omfattande observabilitet.
Nyckeln till en lyckad implementation av Saga är att förstå dina konsistenskrav, noggrant designa kompensationslogiken och välja rätt approach för ditt användningsfall. Med korrekt implementation 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-mönstret - Martin Fowler
- Eventuate Tram Saga Framework
- Temporal Workflow Engine
- AWS Step Functions Dokumentation
- Go-fuskbladet
- Go Generics: Användningsområden och Mönster
- Jämförelse av Go ORM:er 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