Padrão Saga em Transações Distribuídas – Com Exemplos em Go

Transações em Microsserviços com o padrão Saga

Conteúdo da página

O padrão Saga fornece uma solução elegante ao dividir transações distribuídas em uma série de transações locais com ações compensatórias.

Em vez de confiar em bloqueios distribuídos que podem bloquear operações entre serviços, o Saga permite consistência eventual por meio de uma sequência de etapas reversíveis, tornando-o ideal para processos de negócios de longa duração.

Em arquiteturas de microsserviços, manter a consistência dos dados entre serviços é um dos problemas mais desafiadores. As transações ACID tradicionais não funcionam quando as operações abrangem vários serviços com bancos de dados independentes, deixando os desenvolvedores em busca de abordagens alternativas para garantir a integridade dos dados.

Este guia demonstra a implementação do padrão Saga em Go com exemplos práticos que cobrem tanto as abordagens de orquestração quanto de coreografia. Se você precisar de uma referência rápida para os fundamentos do Go, a Go Cheat Sheet fornece uma visão geral útil.

construction worker with distributed transactions Esta bela imagem foi gerada pelo modelo de IA Flux 1 dev.

Compreendendo o Padrão Saga

O padrão Saga foi originalmente descrito por Hector Garcia-Molina e Kenneth Salem em 1987. No contexto de microsserviços, é uma sequência de transações locais onde cada transação atualiza dados dentro de um único serviço. Se qualquer etapa falhar, transações compensatórias são executadas para desfazer os efeitos das etapas anteriores.

Ao contrário das transações distribuídas tradicionais que usam commit em duas fases (2PC), o Saga não mantém bloqueios entre serviços, tornando-o adequado para processos de negócios de longa duração. O trade-off é consistência eventual em vez de consistência forte.

Características Principais

  • Sem Bloqueios Distribuídos: Cada serviço gerencia sua própria transação local
  • Ações Compensatórias: Cada operação tem um mecanismo de rollback correspondente
  • Consistência Eventual: O sistema eventualmente atinge um estado consistente
  • Longa Duração: Adequado para processos que levam segundos, minutos ou até horas

Abordagens de Implementação do Saga

Existem duas abordagens principais para implementar o padrão Saga: orquestração e coreografia.

Padrão de Orquestração

Na orquestração, um coordenador central (orquestrador) gerencia todo o fluxo da transação. O orquestrador é responsável por:

  • Invocar serviços na ordem correta
  • Lidar com falhas e acionar compensações
  • Manter o estado do saga
  • Coordenar retries e timeouts

Vantagens:

  • Controle e visibilidade centralizados
  • Mais fácil de entender e depurar
  • Melhor tratamento de erros e recuperação
  • Teste mais simples do fluxo geral

Desvantagens:

  • Ponto único de falha (embora isso possa ser mitigado)
  • Serviço adicional para manter
  • Pode se tornar um gargalo para fluxos complexos

Exemplo em Go:

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

func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
    sagaID := generateSagaID()
    
    // Step 1: Create order
    orderID, err := o.orderService.Create(order)
    if err != nil {
        return err
    }
    
    // Step 2: Reserve inventory
    if err := o.inventoryService.Reserve(order.Items); err != nil {
        o.orderService.Cancel(orderID) // Compensate
        return err
    }
    
    // Step 3: Process payment
    paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
    if err != nil {
        o.inventoryService.Release(order.Items) // Compensate
        o.orderService.Cancel(orderID)          // Compensate
        return err
    }
    
    // Step 4: Create shipment
    if err := o.shippingService.CreateShipment(orderID); err != nil {
        o.paymentService.Refund(paymentID)      // Compensate
        o.inventoryService.Release(order.Items) // Compensate
        o.orderService.Cancel(orderID)          // Compensate
        return err
    }
    
    return nil
}

Padrão de Coreografia

Na coreografia, não há um coordenador central. Cada serviço sabe o que fazer e se comunica por meio de eventos. Os serviços ouvem os eventos e reagem de acordo. Essa abordagem orientada a eventos é particularmente poderosa quando combinada com plataformas de streaming de mensagens como AWS Kinesis, que fornecem infraestrutura escalável para distribuição de eventos entre microsserviços. Para um guia completo sobre a implementação de microsserviços orientados a eventos com Kinesis, consulte Building Event-Driven Microservices with AWS Kinesis.

Vantagens:

  • Descentralizado e escalável
  • Sem ponto único de falha
  • Os serviços permanecem fracamente acoplados
  • Ajuste natural para arquiteturas orientadas a eventos

Desvantagens:

  • Mais difícil de entender o fluxo geral
  • Difícil de depurar e rastrear
  • Tratamento de erros complexo
  • Risco de dependências cíclicas

Exemplo com Arquitetura Orientada a Eventos:

// 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) // Compensation
}

// 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 {
    // Compensation: refund payment
    return s.client.Refund(event.PaymentID)
}

Estratégias de Compensação

A compensação é o cerne do padrão Saga. Cada operação deve ter uma compensação correspondente que possa reverter seus efeitos.

Tipos de Compensação

  1. Operações Reversíveis: Operações que podem ser desfeitas diretamente

    • Exemplo: Liberando estoque reservado, reembolsando pagamentos
  2. Ações Compensatórias: Operações diferentes que alcançam o efeito inverso

    • Exemplo: Cancelando um pedido em vez de excluí-lo
  3. Compensação Pessimista: Pré-alocar recursos que podem ser liberados

    • Exemplo: Reservar estoque antes de cobrar o pagamento
  4. Compensação Otimista: Executar operações e compensar se necessário

    • Exemplo: Cobrar o pagamento primeiro, reembolsar se o estoque estiver indisponível

Requisitos de Idempotência

Todas as operações e compensações devem ser idempotentes. Isso garante que a repetição de uma operação falha não cause efeitos duplicados.

func (s *PaymentService) Refund(paymentID string) error {
    // Check if already refunded
    payment, err := s.getPayment(paymentID)
    if err != nil {
        return err
    }
    
    if payment.Status == "refunded" {
        return nil // Already refunded, idempotent
    }
    
    // Process refund
    return s.processRefund(paymentID)
}

Melhores Práticas

1. Gerenciamento de Estado do Saga

Mantenha o estado de cada instância de saga para rastrear o progresso e permitir a recuperação. Ao persistir o estado do saga em um banco de dados, escolher o ORM correto é crucial para desempenho e manutenção. Para implementações baseadas em PostgreSQL, considere a comparação em Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc para selecionar a melhor opção para suas necessidades de armazenamento de estado do saga:

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. Tratamento de Timeout

Implemente timeouts para cada etapa para evitar que os sagas fiquem pendentes indefinidamente:

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():
        // Timeout occurred, compensate
        if err := step.Compensate(); err != nil {
            return fmt.Errorf("compensation failed: %w", err)
        }
        return fmt.Errorf("step %s timed out after %v", step.Name(), o.timeout)
    }
}

3. Lógica de Retry

Implemente backoff exponencial para falhas transitórias:

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("operation failed after %d retries", maxRetries)
}

4. Event Sourcing para Estado do Saga

Use event sourcing para manter um registro de auditoria completo. Ao implementar stores de eventos e mecanismos de replay, os generics do Go podem ajudar a criar código de manipulação de eventos seguro quanto ao tipo e reutilizável. Para padrões avançados usando generics em Go, consulte Go Generics: Use Cases and Patterns.

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("failed to marshal payload: %w", err)
    }
    
    version, err := s.store.GetNextVersion(sagaID)
    if err != nil {
        return fmt.Errorf("failed to get 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("failed to get events: %w", err)
    }
    
    saga := NewSaga()
    for _, event := range events {
        if err := saga.Apply(event); err != nil {
            return nil, fmt.Errorf("failed to apply event: %w", err)
        }
    }
    
    return saga, nil
}

5. Monitoramento e Observabilidade

Implemente registro e rastreamento abrangentes:

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 started")
    
    // ... saga execution
    
    return nil
}

Padrões Comuns e Anti-Padrões

Padrões a Seguir

  • Padrão de Coordenador de Saga: Use um serviço dedicado para orquestração
  • Padrão Outbox: Garanta publicação confiável de eventos
  • Chaves de Idempotência: Use chaves únicas para todas as operações
  • Máquina de Estados do Saga: Modele o saga como uma máquina de estados

Anti-Padrões a Evitar

  • Compensação Síncrona: Não espere pela conclusão da compensação
  • Sagas Aninhadas: Evite sagas chamando outros sagas (use sub-sagas em vez disso)
  • Estado Compartilhado: Não compartilhe estado entre etapas do saga
  • Etapas de Longa Duração: Divida etapas que demoram muito

Ferramentas e Frameworks

Vários frameworks podem ajudar na implementação de padrões Saga:

  • Temporal: Plataforma de orquestração de workflows com suporte nativo para Saga
  • Zeebe: Motor de workflow para orquestração de microsserviços
  • Eventuate Tram: Framework Saga para Spring Boot
  • AWS Step Functions: Orquestração de workflows serverless
  • Apache Camel: Framework de integração com suporte a Saga

Para serviços de orquestrador que precisam de interfaces de linha de comando (CLI) para gerenciamento e monitoramento, Building CLI Applications in Go with Cobra & Viper fornece excelentes padrões para criar ferramentas de linha de comando para interagir com orquestradores de saga.

Ao implantar microsserviços baseados em saga no Kubernetes, a implementação de um service mesh pode melhorar significativamente a observabilidade, segurança e gerenciamento de tráfego. Implementing Service Mesh with Istio and Linkerd aborda como os service meshes complementam os padrões de transação distribuída, fornecendo preocupações transversais como rastreamento distribuído e circuit breaking.

Quando Usar o Padrão Saga

Use o padrão Saga quando:

  • ✅ Operações abrangem múltiplos microsserviços
  • ✅ Processos de negócios de longa duração
  • ✅ Consistência eventual é aceitável
  • ✅ Você precisa evitar bloqueios distribuídos
  • ✅ Os serviços possuem bancos de dados independentes

Evite quando:

  • ❌ Consistência forte é necessária
  • ❌ Operações são simples e rápidas
  • ❌ Todos os serviços compartilham o mesmo banco de dados
  • ❌ A lógica de compensação é muito complexa

Conclusão

O padrão Saga é essencial para gerenciar transações distribuídas em arquiteturas de microsserviços. Embora introduza complexidade, fornece uma solução prática para manter a consistência dos dados nas fronteiras dos serviços. Escolha a orquestração para melhor controle e visibilidade, ou a coreografia para escalabilidade e acoplamento fraco. Sempre garanta que as operações sejam idempotentes, implemente uma lógica de compensação adequada e mantenha uma observabilidade abrangente.

A chave para uma implementação bem-sucedida do Saga é compreender seus requisitos de consistência, projetar cuidadosamente a lógica de compensação e escolher a abordagem correta para seu caso de uso. Com uma implementação adequada, o Saga permite que você construa microsserviços resilientes e escaláveis que mantenham a integridade dos dados em sistemas distribuídos.

Assinar

Receba novos artigos sobre sistemas, infraestrutura e engenharia de IA.