Capítulo 14: Mensageria Assíncrona com RabbitMQ ou Apache Kafka
Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Requisição Gin: POST /pedidos"] --> B["Transação SQL Atômica (GORM)"]
subgraph OUTBOX["Padrão Transacional Outbox (Evita Dual-Write)"]
B --> C["1. Grava Pedido na Tabela 'pedidos'"]
B --> D["2. Grava Evento na Tabela 'outbox_events'"]
end
D --> E["Outbox Worker (Go Poller / CDC Debezium)"]
E --> F["amqp.PublishWithContext (RabbitMQ Exchange)"]
subgraph BROKER["RabbitMQ Broker (AMQP 0-9-1)"]
F --> G["Direct / Topic Exchange"]
G --> H["Fila Principal: 'pedidos_queue' (Durable)"]
H -->|"Falhas Repetidas / Nack"| I["Dead Letter Exchange (DLX)"]
I --> J["Fila DLQ: 'pedidos_dlq' (Inspeção Humana)"]
end
H --> K["Consumidor Go Worker (QoS Prefetch: 10)"]
K --> L{"Processamento com Idempotência"}
L -->|"Sucesso"| M["msg.Ack(false) -> Remove da Fila"]
L -->|"Erro Transitório"| N["msg.Nack(false, true) -> Reenfileira"]
L -->|"Payload Inválido"| O["msg.Nack(false, false) -> Desvia para DLQ"]
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style D fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style E fill:#fffde7,stroke:#fbc02d,stroke-width:2px
style F fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style H fill:#e0f2f1,stroke:#009688,stroke-width:2px
style I fill:#ffebee,stroke:#e53935,stroke-width:2px
style J fill:#b71c1c,stroke:#b71c1c,stroke-width:2px,color:#fff
style K fill:#d1c4e9,stroke:#512da8,stroke-width:2px
style M fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style N fill:#fffde7,stroke:#fbc02d,stroke-width:2px
style O fill:#ffebee,stroke:#e53935,stroke-width:2px
🏛️ 1. O Dilema Arquitetural: RabbitMQ vs Apache Kafka
Em arquiteturas modernas de microsserviços em Go, sistemas orientados a eventos (Event-Driven Architecture) desacoplam o processamento síncrono da API HTTP de tarefas intensivas ou pipelines analíticos. A escolha entre RabbitMQ e Kafka define a semântica de entrega e o modelo de escalabilidade da plataforma:
| Dimensão de Comparação | RabbitMQ (Message Broker / AMQP) | Apache Kafka (Distributed Event Streaming) |
|---|---|---|
| Modelo Conceitual | Smart Broker, Dumb Consumer (O broker gerencia o estado). | Dumb Broker, Smart Consumer (O cliente gerencia seu offset). |
| Retenção de Mensagens | A mensagem é deletada imediatamente após o Ack. |
Mantém logs persistidos em disco por dias/meses (Message Replay). |
| Roteamento | Exchanges avançadas (Direct, Fanout, Topic, Headers). | Roteamento por Chave de Partição (Partition Key). |
| Throughput / Vazão | Dezenas de milhares de msgs/segundo. | Milhões de eventos/segundo por partição. |
| Ordenação | Garantida estritamente por fila única (sem paralelismo concorrente). | Garantida dentro da mesma partição por chave. |
| Caso de Uso Ideal | Filas de processamento assíncrono, comandos, e-mails, webhooks. | Big Data, telemetria IoT, Event Sourcing e auditoria imutável. |
🛡️ 2. A Armadilha do Dual-Write e o Padrão Transacional Outbox
Um dos problemas mais comuns em microsserviços é a inconsistência de dados por escrita dupla:
// ❌ ANTI-PATTERN: Inconsistência Crítica Garantida
func CriarPedido(db *gorm.DB, ch *amqp.Channel, pedido *Pedido) error {
if err := db.Create(pedido).Error; err != nil {
return err
}
// E se a rede cair ou o RabbitMQ reiniciar EXATAMENTE AQUI?
// O pedido estará salvo no banco, mas o evento NUNCA será publicado!
return ch.PublishWithContext(...)
}
A Solução Canônica: Transactional Outbox Pattern
Ambas as gravações ocorrem dentro da mesma transação SQL local no banco de dados. Um worker separado lê a tabela outbox_events e publica as mensagens no broker:
-- Tabela de Outbox no PostgreSQL
CREATE TABLE outbox_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agregado_tipo VARCHAR(50) NOT NULL,
agregado_id VARCHAR(50) NOT NULL,
payload JSONB NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PENDENTE',
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
⚙️ 3. Implementação Resiliente de Consumo com amqp091-go
No Go, utilizamos a biblioteca oficial github.com/rabbitmq/amqp091-go. Para obter alto rendimento sem sobrecarregar a memória do worker, é mandatório configurar o QoS (Prefetch Count):
package consumer
import (
"encoding/json"
"log"
amqp "github.com/rabbitmq/amqp091-go"
)
type EventoFaturamento struct {
PedidoID uint `json:"pedido_id"`
Valor float64 `json:"valor"`
}
// IniciarConsumidor configura canal, fila durável, DLQ e prefetch
func IniciarConsumidor(conn *amqp.Connection) error {
ch, err := conn.Channel()
if err != nil {
return err
}
defer ch.Close()
// 1. Configura QoS: limita o buffer local da goroutine a 20 mensagens não confirmadas
if err := ch.Qos(20, 0, false); err != nil {
return err
}
// 2. Declaração da Fila Principal com Dead-Letter Exchange associada
argumentos := amqp.Table{
"x-dead-letter-exchange": "dlx.pedidos",
"x-dead-letter-routing-key": "pedidos.mortos",
}
fila, err := ch.QueueDeclare(
"pedidos.faturamento",
true, // Durable: sobrevive a restart do RabbitMQ
false, // Auto-delete
false, // Exclusive
false, // No-wait
argumentos,
)
if err != nil {
return err
}
mensagens, err := ch.Consume(
fila.Name,
"worker-faturamento-1",
false, // ⚠️ Auto-Ack = FALSE (Ack manual obrigatório para resiliência)
false,
false,
false,
nil,
)
if err != nil {
return err
}
log.Println("[MESSAGING] Worker escutando pedidos com Ack manual ativo...")
// 3. Processamento concorrente das entregas
for entrega := range mensagens {
var evento EventoFaturamento
if err := json.Unmarshal(entrega.Body, &evento); err != nil {
// Erro estrutural grave (Poison Message): Nack sem requeue envia para DLQ
log.Printf("[ERRO] Payload malformado: %v", err)
entrega.Nack(false, false)
continue
}
if err := processarComIdempotencia(evento); err != nil {
// Erro de rede ou indisponibilidade temporária: devolve para a fila
log.Printf("[ALERTA] Falha temporária no pedido %d: %v", evento.PedidoID, err)
entrega.Nack(false, true)
continue
}
// Confirmação atômica de entrega e remoção da fila
entrega.Ack(false)
}
return nil
}
🔁 4. Idempotência do Consumidor
Devido à semântica At-Least-Once Delivery (entrega ao menos uma vez) do RabbitMQ e Kafka, uma falha de rede temporária no momento em que o consumidor emite o Ack fará com que o broker reenfileire a mensagem. Portanto, o consumidor receberá e processará a mesma mensagem uma segunda vez.
Para evitar duplicações financeiras ou pedidos duplicados:
- Toda mensagem deve carregar um identificador único de idempotência (
Idempotency-KeyouUUIDdo evento). - O consumidor deve verificar no banco relacional ou Redis se aquele
evento_idjá foi registrado antes de executar qualquer transação.