DevGuideTK
← Trilha Backend

Filas e mensageria

RabbitMQ, Kafka e o que muda entre uma fila de tarefas e um log de eventos.

Os exemplos consomem a fila pedidos.criados do RabbitMQ, com o cliente oficial ou o mais adotado de cada linguagem. O consumidor recebe a mensagem, processa de forma idempotente e só então confirma.

Subir um RabbitMQ para testar

docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4-management

O painel fica em http://localhost:15672 (usuário e senha guest). Em seguida, crie o destino das mensagens rejeitadas e uma política que as envia para lá, para todas as filas pedidos.*:

# A troca e a fila que recebem as mensagens rejeitadas
docker exec rabbitmq rabbitmqadmin declare exchange --name mortas --type fanout
docker exec rabbitmq rabbitmqadmin declare queue --name mensagens.mortas
docker exec rabbitmq rabbitmqadmin declare binding --source mortas \
  --destination-type queue --destination mensagens.mortas --routing-key ""

# A política que manda para "mortas" o que for rejeitado nas filas pedidos.*
docker exec rabbitmq rabbitmqctl set_policy mensagens-mortas "^pedidos\." \
  '{"dead-letter-exchange":"mortas"}' --apply-to queues

A política fica no intermediário, fora do código. Assim é possível mudá-la sem redeclarar as filas, o que o RabbitMQ não permite quando os argumentos de uma fila existente mudam.

Um consumidor que confirma só depois de processar

import amqp from "amqplib";

const FILA = "pedidos.criados";

const conexao = await amqp.connect(process.env.RABBITMQ_URL!);
const canal = await conexao.createChannel();

await canal.assertQueue(FILA, { durable: true });
await canal.prefetch(10); // no máximo 10 mensagens sem confirmação

await canal.consume(FILA, async (mensagem) => {
  if (!mensagem) return;
  try {
    const evento = JSON.parse(mensagem.content.toString());
    await processarUmaVez(mensagem.properties.messageId, evento);
    canal.ack(mensagem);
  } catch (erro) {
    console.error("falha ao processar", erro);
    canal.nack(mensagem, false, false); // vai para a fila de mensagens mortas
  }
}, { noAck: false });

O que muda de uma linguagem para outra

BibliotecaModelo do consumidorConfirmação manual
NodeamqplibFunção chamada a cada mensagem{ noAck: false }
PythonpikaFunção chamada a cada mensagem, numa conexão bloqueanteauto_ack=False
Goamqp091-goCanal Go, lido num laçoTerceiro argumento de Consume igual a false
Javaamqp-clientDeliverCallbackSegundo argumento de basicConsume igual a false
C#RabbitMQ.Client 7Evento ReceivedAsync, totalmente assíncronoautoAck: false

Três detalhes que costumam passar despercebidos:

  • Confirmação automática (noAck: true, auto_ack=True) remove a mensagem no momento da entrega. Se o consumidor cair durante o processamento, a mensagem se perde. Use sempre a confirmação manual, depois do trabalho feito.
  • prefetch limita quantas mensagens um consumidor recebe sem ter confirmado. Sem limite, um consumidor pode receber milhares de mensagens de uma vez, ficar sem memória e, ao cair, devolver todas para a fila.
  • No Python, a conexão bloqueante do pika não é segura para uso entre threads. Para processar em paralelo, use um processo por consumidor, ou entregue o trabalho a outra thread e confirme pela própria conexão com add_callback_threadsafe.

Nos exemplos, toda falha vai direto para a fila de mensagens mortas. Para falhas temporárias, o nível Soluções descreve as novas tentativas com atraso, que no RabbitMQ usam uma fila de espera com tempo de expiração ou o limite de entregas das filas quorum.

Processar uma vez: idempotência com o banco

A função processarUmaVez registra o identificador da mensagem numa tabela com chave única, na mesma transação do efeito. Se a mensagem chegar de novo, a inserção falha por duplicidade e o efeito não se repete.

CREATE TABLE mensagens_processadas (
  mensagem_id    TEXT PRIMARY KEY,
  processada_em  TIMESTAMPTZ NOT NULL DEFAULT now()
);
export async function processarUmaVez(mensagemId: string, evento: PedidoCriado) {
  if (!mensagemId) throw new Error("mensagem sem identificador");

  await db.transaction(async (tx) => {
    const registro = await tx.query(
      `INSERT INTO mensagens_processadas (mensagem_id)
       VALUES ($1)
       ON CONFLICT (mensagem_id) DO NOTHING`,
      [mensagemId],
    );

    if (registro.rowCount === 0) {
      return; // já foi processada antes: não faz nada
    }

    await tx.query(
      "UPDATE produtos SET reservado = reservado + $1 WHERE id = $2",
      [evento.quantidade, evento.produtoId],
    );
  });
}
DetalhePor quê
O registro e o efeito na mesma transaçãoSe o efeito falhar, o registro também é desfeito, e a mensagem pode ser processada de novo
ON CONFLICT DO NOTHINGUma reentrega não gera erro, só é ignorada
O identificador vem do produtorO produtor define o messageId ao publicar. Sem ele, não há como reconhecer a mesma mensagem
Limpeza periódicaA tabela cresce; registros mais antigos que o tempo máximo de reentrega podem ser apagados

Quando o efeito está fora do banco, como enviar um e-mail ou chamar uma API de pagamento, a transação não o cobre. Nesses casos, use a chave de idempotência do próprio serviço externo, quando ele oferecer, ou aceite que, numa falha rara, o efeito pode se repetir.

Publicar com identificador e persistência

Do lado do produtor, duas propriedades são essenciais: um identificador único por mensagem e a marcação como persistente, para que ela sobreviva a uma reinicialização do intermediário.

import { randomUUID } from "node:crypto";

canal.sendToQueue(
  "pedidos.criados",
  Buffer.from(JSON.stringify({ pedidoId: 981, produtoId: 7, quantidade: 2 })),
  {
    messageId: randomUUID(),
    persistent: true,
    contentType: "application/json",
  },
);

Para garantir que o intermediário recebeu a mensagem, use um canal com confirmação de publicação (createConfirmChannel no amqplib), que avisa quando a mensagem foi gravada. E, para não perder a mensagem entre a gravação no banco e a publicação, o padrão outbox descrito no nível Soluções.

Lista de revisão

Consumidor

  • A confirmação é manual e acontece só depois do processamento?
  • Existe limite de mensagens sem confirmação (prefetch)?
  • O processamento é idempotente, com o registro na mesma transação do efeito?
  • Falhas permanentes vão para a fila de mensagens mortas, sem voltar para a fila principal?
  • Falhas temporárias são tentadas de novo com atraso e com limite de tentativas?

Produtor

  • Cada mensagem tem um identificador único definido pelo produtor?
  • As mensagens são persistentes e as filas são duráveis?
  • A publicação é confirmada pelo intermediário?
  • A mensagem e os dados são gravados juntos, pelo padrão outbox?

Operação

  • Alguém é avisado quando a fila de mensagens mortas recebe mensagens?
  • O tamanho da fila e a idade da mensagem mais antiga são monitorados?
  • Os consumidores toleram mensagens fora de ordem?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Construir um consumidor confiável

    Quero construir um consumidor de mensagens confiável, testando cada comportamento.
    
    Pergunte qual linguagem e qual intermediário eu uso.
    
    Vamos em etapas. Em cada uma, deixe que eu tente e depois mostre a sua versão:
    1. Subir o intermediário com Docker e publicar mensagens de teste.
    2. Um consumidor com confirmação manual e limite de mensagens sem confirmação.
    3. Derrubar o consumidor no meio do processamento e observar a reentrega.
    4. Tornar o consumidor idempotente com uma tabela de mensagens processadas, na mesma transação do efeito.
    5. Configurar a fila de mensagens mortas e mandar para ela uma mensagem com JSON inválido.
    6. Implementar novas tentativas com atraso para falhas temporárias.
    7. Encerrar o consumidor de forma graciosa, terminando a mensagem atual antes de sair.
    
    Em cada etapa, diga o que observar no painel do intermediário.
  • Ordem das mensagens por chave

    Quero garantir que os eventos de um mesmo pedido sejam processados em ordem, sem perder o paralelismo entre pedidos diferentes.
    
    Pergunte qual intermediário eu uso.
    
    Explique, com código:
    1. Por que vários consumidores em paralelo e as novas tentativas embaralham a ordem.
    2. Como garantir a ordem por chave no meu intermediário (partições e chave da mensagem no Kafka, grupos de mensagens em filas FIFO, ou a alternativa no RabbitMQ).
    3. O efeito disso na escala: quantos consumidores podem trabalhar em paralelo.
    4. Como tornar o consumidor tolerante a eventos fora de ordem, guardando a versão ou a data do último evento aplicado.
    5. O que fazer com um evento de uma chave que falha e bloqueia os seguintes da mesma chave.
    
    Use os eventos pedido criado, pago e cancelado como exemplo.
  • Revisar código de mensageria

    Vou colar abaixo código que publica ou consome mensagens. Revise com foco em confiabilidade.
    
    Verifique, nesta ordem, e para cada item diga se está correto, incorreto ou ausente, citando a linha:
    1. A confirmação é manual e acontece depois do processamento?
    2. Há limite de mensagens sem confirmação?
    3. O consumidor é idempotente? O registro de idempotência está na mesma transação do efeito?
    4. Falhas permanentes e temporárias são tratadas de formas diferentes? Há limite de tentativas e fila de mensagens mortas?
    5. As mensagens têm identificador único e são persistentes? A publicação é confirmada?
    6. Há risco de gravar no banco e não publicar, ou o contrário?
    7. O consumidor tolera mensagens fora de ordem?
    8. O encerramento do consumidor é gracioso?
    
    Para cada problema, descreva o cenário concreto em que ele perde, duplica ou trava mensagens, e mostre a correção. Não reescreva o que já está correto.
    
    Código:
    [cole aqui]