Transações Distribuídas - Padrões de Design para Manter a Consistência

15 min leitura | 2024.12.25

Desafios das Transações Distribuídas

Em microsserviços, uma única operação de negócio pode abranger múltiplos serviços. Como cada serviço possui seu próprio banco de dados, não é possível usar transações ACID tradicionais.

flowchart LR
    Order["1. Serviço de Pedidos<br/>Criar pedido"]
    Inventory["2. Serviço de Estoque<br/>Reduzir estoque"]
    Payment["3. Serviço de Pagamento<br/>Processar pagamento"]
    Shipping["4. Serviço de Entrega<br/>Agendar entrega"]

    Order --> Inventory --> Payment --> Shipping

    Question["❓ E se falhar em algum lugar?<br/>Fazer rollback de tudo?"]

Two-Phase Commit (2PC)

Protocolo onde todos os participantes confirmam estar prontos antes de fazer commit simultaneamente.

sequenceDiagram
    participant C as Coordenador
    participant A as Participante A
    participant B as Participante B

    C->>A: 1. Prepare
    C->>B: 1. Prepare
    A-->>C: 2. Ready
    B-->>C: 2. Ready
    C->>A: 3. Commit
    C->>B: 3. Commit
    A-->>C: 4. Done
    B-->>C: 4. Done

Problemas do 2PC

ProblemaDescrição
BloqueioParticipantes ficam travados enquanto aguardam resposta do coordenador
Ponto único de falhaSe o coordenador cair, todo o sistema para
Baixo desempenhoRequer comunicação síncrona
Limitações em ambientes distribuídosDifícil lidar com particionamento de rede

2PC não é recomendado para microsserviços modernos

Padrão Saga

Divide uma transação longa em uma série de transações locais. Em caso de falha, faz rollback através de transações compensatórias.

flowchart LR
    subgraph Normal["Fluxo Normal"]
        T1["T1<br/>Criar pedido"] --> T2["T2<br/>Reservar estoque"] --> T3["T3<br/>Pagamento"] --> T4["T4<br/>Agendar entrega"]
    end
flowchart LR
    subgraph Failure["Em caso de falha (falha em T3)"]
        T1b["T1"] --> T2b["T2"] --> T3b["T3<br/>❌ Falha"] --> C2["C2<br/>Liberar estoque"] --> C1["C1<br/>Cancelar pedido"]
    end

Abordagem por Orquestração

Um orquestrador central controla a Saga.

class OrderSaga {
  async execute(orderData) {
    const sagaId = uuid();
    let currentStep = 0;

    try {
      // Step 1: Criar pedido
      const order = await orderService.create(orderData);
      currentStep = 1;

      // Step 2: Reservar estoque
      await inventoryService.reserve(order.items);
      currentStep = 2;

      // Step 3: Pagamento
      await paymentService.process(order.total);
      currentStep = 3;

      // Step 4: Agendar entrega
      await shippingService.schedule(order);
      currentStep = 4;

      return { success: true, orderId: order.id };

    } catch (error) {
      // Executar transações compensatórias
      await this.compensate(currentStep, order);
      throw error;
    }
  }

  async compensate(step, order) {
    if (step >= 3) await paymentService.refund(order.id);
    if (step >= 2) await inventoryService.release(order.items);
    if (step >= 1) await orderService.cancel(order.id);
  }
}

Abordagem por Coreografia

Cada serviço publica e assina eventos para coordenar.

flowchart TB
    subgraph Success["Fluxo Normal"]
        OS1["Serviço de Pedidos"] --> E1["OrderCreated"]
        E1 --> IS1["Serviço de Estoque"] --> E2["InventoryReserved"]
        E2 --> PS1["Serviço de Pagamento"] --> E3["PaymentProcessed"]
        E3 --> SS1["Serviço de Entrega"] --> E4["ShippingScheduled"]
    end
flowchart TB
    subgraph Failure["Em caso de falha"]
        PS2["Serviço de Pagamento"] --> EF1["PaymentFailed"]
        EF1 --> IS2["Serviço de Estoque"] --> EF2["InventoryReleased<br/>(compensação)"]
        EF2 --> OS2["Serviço de Pedidos"] --> EF3["OrderCancelled<br/>(compensação)"]
    end

Comparação

AspectoOrquestraçãoCoreografia
VisibilidadeFluxo claroFluxo distribuído
AcoplamentoDepende do orquestradorBaixo acoplamento entre serviços
ComplexidadeLógica simplesDesign de eventos complexo
DepuraçãoFácilDifícil

Transações Compensatórias

Em vez de fazer rollback em caso de falha, executa a operação inversa.

// Operação original
async function reserveInventory(items) {
  for (const item of items) {
    await db.inventory.update({
      where: { productId: item.productId },
      data: { quantity: { decrement: item.quantity } }
    });
  }
}

// Transação compensatória
async function releaseInventory(items) {
  for (const item of items) {
    await db.inventory.update({
      where: { productId: item.productId },
      data: { quantity: { increment: item.quantity } }
    });
  }
}

Considerações sobre Compensação

Nem todas as operações podem ser compensadas:

OperaçãoMétodo de compensação
Envio de e-mailEnviar “e-mail de cancelamento”
Início de trabalho físicoRequer intervenção manual
Chamadas a APIs externasRequer API de compensação do sistema externo

Padrão Outbox

Garante a execução confiável de atualizações de banco de dados e publicação de eventos.

// 1. Executar ambos dentro de uma transação
async function createOrder(orderData) {
  await db.transaction(async (tx) => {
    // Salvar dados de negócio
    const order = await tx.orders.create(orderData);

    // Salvar evento na tabela outbox
    await tx.outbox.create({
      eventType: 'OrderCreated',
      payload: JSON.stringify(order),
      status: 'pending'
    });
  });
}

// 2. Processo separado faz polling na outbox
async function publishOutboxEvents() {
  const events = await db.outbox.findMany({
    where: { status: 'pending' }
  });

  for (const event of events) {
    await messageQueue.publish(event.eventType, event.payload);
    await db.outbox.update({
      where: { id: event.id },
      data: { status: 'published' }
    });
  }
}

Consistência Eventual

Permite inconsistência temporária em vez de consistência imediata, garantindo que a consistência seja alcançada eventualmente.

Modelo de ConsistênciaDescrição
Consistência forteTodos veem os mesmos dados imediatamente após a escrita
Consistência eventualApós a escrita, eventualmente todos verão os mesmos dados (tolerando inconsistência temporária)

Lidando com Consistência Eventual

// Exemplo de tratamento no lado da UI
async function placeOrder(orderData) {
  const result = await api.createOrder(orderData);

  // Exibir como "processando" em vez de confirmado imediatamente
  return {
    orderId: result.orderId,
    status: 'processing',
    message: 'Seu pedido foi recebido. Você será notificado por e-mail após a confirmação.'
  };
}

Resumo

Transações distribuídas são um desafio difícil em arquiteturas de microsserviços. 2PC não é adequado para sistemas distribuídos modernos, sendo recomendado o padrão Saga com transações compensatórias. É importante aceitar a consistência eventual e escolher o modelo de consistência adequado aos requisitos de negócio.

← Voltar para a lista