Wzorzec Saga w rozproszonych transakcjach – z przykładami w Go

Transakcje w mikrousługach z wykorzystaniem wzorca Saga

Page content

Wzorzec Saga Saga pattern dostarcza eleganckiego rozwiązania, dzieląc transakcje rozproszone na serię lokalnych transakcji z akcjami kompensacyjnymi.

Zamiast polegać na rozproszonych blokadach, które mogą zablokować operacje między usługami, Saga umożliwia osiągnięcie ostatecznej spójności poprzez sekwencję odwracalnych kroków, co czyni ją idealną dla długotrwałych procesów biznesowych.

W architekturach mikroserwisowych utrzymanie spójności danych między usługami jest jednym z największych wyzwań. Tradycyjne transakcje ACID nie działają poprawnie, gdy operacje obejmują wiele usług z niezależnymi bazami danych, co zmusza programistów do poszukiwania alternatywnych podejść zapewniających integralność danych.

Ten przewodnik demonstruje implementację wzorca Saga w języku Go z praktycznymi przykładami obejmującymi zarówno podejście oparte na orkiestracji, jak i choreografię. Jeśli potrzebujesz szybkiego zestawienia podstaw Go, Cheat Sheet Go dostarcza pomocne omawianie zagadnień.

budowniczy z transakcjami rozproszonymi To ładne zdjęcie zostało wygenerowane przez model AI Flux 1 dev.

Zrozumienie Wzorca Saga

Wzorzec Saga został pierwotnie opisany przez Hectora Garcíę-Molinę i Kennetha Salema w 1987 roku. W kontekście mikroserwisów jest to sekwencja lokalnych transakcji, gdzie każda transakcja aktualizuje dane w ramach jednej usługi. Jeśli którykolwiek krok się nie powiedzie, wykonywane są transakcje kompensacyjne w celu cofnięcia efektów poprzednich kroków.

W przeciwieństwie do tradycyjnych transakcji rozproszonych wykorzystujących dwufazowe zatwierdzanie (2PC), Saga nie utrzymuje blokad między usługami, co czyni ją odpowiednią dla długotrwałych procesów biznesowych. Ceną za to jest ostateczna spójność zamiast silnej spójności.

Kluczowe Cechy

  • Brak Rozproszonych Blokad: Każda usługa zarządza własną lokalną transakcją
  • Akcje Kompensacyjne: Każda operacja ma odpowiadający jej mechanizm cofania
  • Ostateczna Spójność: System ostatecznie osiąga stan spójny
  • Długotrwałość: Nadaje się do procesów trwających sekundy, minuty lub nawet godziny

Podejścia do Implementacji Saga

Istnieją dwa główne podejścia do implementacji wzorca Saga: orkiestracja i choreografia.

Wzorzec Orkiestracji

W orkiestracji centralny koordynator (orkiestратор) zarządza całym przepływem transakcji. Orkiestrator jest odpowiedzialny za:

  • Wywoływanie usług w poprawnej kolejności
  • Obsługę błędów i inicjowanie kompensacji
  • Utrzymywanie stanu sagi
  • Koordynowanie ponownych prób i limitów czasu

Zalety:

  • Centralizowana kontrola i widoczność
  • Łatwiejsze zrozumienie i debugowanie
  • Lepsza obsługa błędów i odzyskiwanie
  • Prostsze testowanie całego przepływu

Wady:

  • Pojedynczy punkt awarii (choć można go złagodzić)
  • Dodatkowa usługa do utrzymania
  • Może stać się wąskim gardłem w złożonych przepływach

Przykład w Go:

type OrderSagaOrchestrator struct {
    orderService    OrderService
    paymentService  PaymentService
    inventoryService InventoryService
    shippingService ShippingService
}

func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
    sagaID := generateSagaID()
    
    // Krok 1: Utwórz zamówienie
    orderID, err := o.orderService.Create(order)
    if err != nil {
        return err
    }
    
    // Krok 2: Zarezerwuj inwentarz
    if err := o.inventoryService.Reserve(order.Items); err != nil {
        o.orderService.Cancel(orderID) // Kompensacja
        return err
    }
    
    // Krok 3: Przetwórz płatność
    paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
    if err != nil {
        o.inventoryService.Release(order.Items) // Kompensacja
        o.orderService.Cancel(orderID)          // Kompensacja
        return err
    }
    
    // Krok 4: Utwórz przesyłkę
    if err := o.shippingService.CreateShipment(orderID); err != nil {
        o.paymentService.Refund(paymentID)      // Kompensacja
        o.inventoryService.Release(order.Items) // Kompensacja
        o.orderService.Cancel(orderID)          // Kompensacja
        return err
    }
    
    return nil
}

Wzorzec Choreografii

W choreografii nie ma centralnego koordynatora. Każda usługa wie, co ma robić i komunikuje się za pomocą zdarzeń. Usługi nasłuchują zdarzeń i reagują odpowiednio. To podejście oparte na zdarzeniach jest szczególnie skuteczne w połączeniu z platformami strumieniowania wiadomości takimi jak AWS Kinesis, które dostarczają skalowalnej infrastruktury do dystrybucji zdarzeń między mikroserwisami. Aby uzyskać kompleksowy przewodnik dotyczący implementacji mikroserwisów opartych na zdarzeniach z wykorzystaniem Kinesis, zobacz Budowanie Mikroserwisów Oparty na Zdarzeniach z AWS Kinesis.

Zalety:

  • Rozproszenie i skalowalność
  • Brak pojedynczego punktu awarii
  • Usługi pozostają luźno powiązane
  • Naturalne dopasowanie do architektur opartych na zdarzeniach

Wady:

  • Trudniejsze zrozumienie całego przepływu
  • Trudne debugowanie i śledzenie
  • Skomplikowana obsługa błędów
  • Ryzyko cyklicznych zależności

Przykład z Architekturą Opartą na Zdarzeniach:

// Usługa Zamówień
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) // Kompensacja
}

// Usługa Płatności
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 {
    // Kompensacja: zwrot płatności
    return s.client.Refund(event.PaymentID)
}

Strategie Kompensacji

Kompensacja jest sercem wzorca Saga. Każda operacja musi mieć odpowiadającą jej kompensację, która może odwrócić jej efekty.

Rodzaje Kompensacji

  1. Operacje Odwracalne: Operacje, które można bezpośrednio cofnąć

    • Przykład: Zwolnienie zarezerwowanego inwentarza, zwrot płatności
  2. Akcje Kompensacyjne: Różne operacje, które osiągają odwrotny efekt

    • Przykład: Anulowanie zamówienia zamiast jego usuwania
  3. Kompensacja Pesymistyczna: Wstępna alokacja zasobów, które mogą zostać zwolnione

    • Przykład: Rezerwacja inwentarza przed naliczeniem płatności
  4. Kompensacja Optymistyczna: Wykonanie operacji i kompensacja w razie potrzeby

    • Przykład: Naliczenie płatności jako pierwsze, zwrot jeśli inwentarz jest niedostępny

Wymagania Idempotentności

Wszystkie operacje i kompensacje muszą być idempotentne. Zapewnia to, że ponowna próba nieudanej operacji nie spowoduje zduplikowanych efektów.

func (s *PaymentService) Refund(paymentID string) error {
    // Sprawdź, czy już zwrócono środki
    payment, err := s.getPayment(paymentID)
    if err != nil {
        return err
    }
    
    if payment.Status == "refunded" {
        return nil // Już zwrócono, idempotentne
    }
    
    // Przetwórz zwrot
    return s.processRefund(paymentID)
}

Najlepsze Praktyki

1. Zarządzanie Stanem Sagi

Utrzymuj stan każdej instancji sagi, aby śledzić postęp i umożliwić odzyskiwanie. Przy persistowaniu stanu sagi do bazy danych, wybór odpowiedniego ORM jest kluczowy dla wydajności i łatwości utrzymania. Dla implementacji opartych na PostgreSQL, rozważ porównanie w Porównanie ORM Go dla PostgreSQL: GORM vs Ent vs Bun vs sqlc, aby wybrać najlepsze rozwiązanie dla potrzeb przechowywania stanu sagi:

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. Obsługa Limitów Czasu

Wdroż limity czasu dla każdego kroku, aby zapobiec zawieszaniu się sag bezterminowo:

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():
        // Wystąpił limit czasu, skompensuj
        if err := step.Compensate(); err != nil {
            return fmt.Errorf("kompensacja nieudana: %w", err)
        }
        return fmt.Errorf("krok %s przekroczył limit czasu po %v", step.Name(), o.timeout)
    }
}

3. Logika Ponownych Prób

Wdroż wykładnicze cofanie (backoff) dla przejściowych awarii:

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("operacja nieudana po %d próbach", maxRetries)
}

4. Event Sourcing dla Stanu Sagi

Wykorzystuj event sourcing do utrzymania kompletnego śladu audytowego. Przy implementacji magazynów zdarzeń i mechanizmów odtwarzania, generyki w Go mogą pomóc w tworzeniu bezpiecznych typowo, wielorazowo wykorzystywalnych kodów obsługi zdarzeń. Aby poznać zaawansowane wzorce wykorzystujące generyki w Go, zobacz Generyki w Go: Przypadki Użycia i Wzorce.

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("błąd serializacji ładunku: %w", err)
    }
    
    version, err := s.store.GetNextVersion(sagaID)
    if err != nil {
        return fmt.Errorf("błąd pobrania wersji: %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("błąd pobrania zdarzeń: %w", err)
    }
    
    saga := NewSaga()
    for _, event := range events {
        if err := saga.Apply(event); err != nil {
            return nil, fmt.Errorf("błąd zastosowania zdarzenia: %w", err)
        }
    }
    
    return saga, nil
}

5. Monitorowanie i Obserwowalność

Wdroż kompleksowe logowanie i śledzenie:

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 rozpoczęta")
    
    // ... wykonanie sagi
    
    return nil
}

Powszechne Wzorce i Antywzorce

Wzorce do Podążania

  • Wzorzec Koordynatora Sagi: Użyj dedykowanej usługi do orkiestracji
  • Wzorzec Outbox: Zapewnij niezawodne publikowanie zdarzeń
  • Klucze Idempotentności: Używaj unikalnych kluczy dla wszystkich operacji
  • Automat Stanu Sagi: Modeluj sagę jako automat stanów

Antywzorce do Unikania

  • Kompensacja Synchroniczna: Nie czekaj na ukończenie kompensacji
  • Sagi Zagnieżdżone: Unikaj sag wywołujących inne sagi (zamiast tego użyj podsag)
  • Współdzielony Stan: Nie dziel stanu między krokami sagi
  • Długotrwałe Kroki: Podziel kroki, które zajmują zbyt dużo czasu

Narzędzia i Frameworki

Kilka frameworków może pomóc w implementacji wzorców Saga:

  • Temporal: Platforma orkiestracji przepływów pracy z wbudowanym wsparciem dla Sagi
  • Zeebe: Silnik przepływów pracy do orkiestracji mikroserwisów
  • Eventuate Tram: Framework Saga dla Spring Boot
  • AWS Step Functions: Orkiestracja przepływów pracy bezserwerowych
  • Apache Camel: Framework integracji z wsparciem dla Sagi

Dla usług orkiestratorów potrzebujących interfejsów CLI do zarządzania i monitorowania, Budowanie Aplikacji CLI w Go z Cobra & Viper dostarcza doskonałe wzorce do tworzenia narzędzi wiersza poleceń do interakcji z orkiestratorami sag.

Podczas wdrażania mikroserwisów opartych na Sage w Kubernetes, wdrożenie siatki usług (service mesh) może znacząco poprawić obserwowalność, bezpieczeństwo i zarządzanie ruchem. Implementacja Siatki Usług z Istio i Linkerd omawia, jak siatki usług uzupełniają wzorce rozproszonych transakcji, dostarczając przekrojowe aspekty takie jak rozproszone śledzenie i przerywanie obwodu.

Kiedy Używać Wzorca Saga

Używaj wzorca Saga, gdy:

  • ✅ Operacje obejmują wiele mikroserwisów
  • ✅ Długotrwałe procesy biznesowe
  • ✅ Ostateczna spójność jest akceptowalna
  • ✅ Należy unikać rozproszonych blokad
  • ✅ Usługi mają niezależne bazy danych

Unikaj, gdy:

  • ❌ Wymagana jest silna spójność
  • ❌ Operacje są proste i szybkie
  • ❌ Wszystkie usługi współdzielą tę samą bazę danych
  • ❌ Logika kompensacji jest zbyt złożona

Podsumowanie

Wzorzec Saga jest niezbędny do zarządzania rozproszonymi transakcjami w architekturach mikroserwisowych. Choć wprowadza złożoność, dostarcza praktycznego rozwiązania do utrzymania spójności danych na granicach usług. Wybierz orkiestrację dla lepszej kontroli i widoczności, lub choreografię dla skalowalności i luźnego powiązania. Zawsze zapewniaj idempotentność operacji, wdrażaj poprawną logikę kompensacji i utrzymuj kompleksową obserwowalność.

Kluczem do sukcesu implementacji Saga jest zrozumienie wymagań dotyczących spójności, staranne zaprojektowanie logiki kompensacji i wybór odpowiedniego podejścia dla danego przypadku użycia. Przy poprawnej implementacji Saga umożliwia budowanie odpornych, skalowalnych mikroserwisów, które utrzymują integralność danych w rozproszonych systemach.

Przydatne Linki

Subskrybuj

Otrzymuj nowe wpisy o systemach, infrastrukturze i inżynierii AI.