Saga-mönstret för distribuerade transaktioner – med exempel i Go

Transaktioner i mikrotjänster med Sagamönstret

Sidinnehåll

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.

byggare med distribuerade transaktioner 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

  1. Reversibla operationer: Operationer som kan ångras direkt

    • Exempel: Frigöra reserverat lager, återbetal betalningar
  2. Kompenserande åtgärder: Olika operationer som uppnår den motsatta effekten

    • Exempel: Avbryt en order istället för att ta bort den
  3. Pessimistisk kompensation: Föralloca resurser som kan frigöras

    • Exempel: Reservera lager innan betalning belastas
  4. 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

Prenumerera

Få nya inlägg om system, infrastruktur och AI-ingenjörskonst.