17.1 RabbitMQ
Resumo
RabbitMQ — message broker para comunicação assíncrona entre serviços pelo protocolo AMQP.
Componentes: Producer envia mensagens para o Exchange, que roteia para a Queue. O Consumer pega e processa.
Tipos de Exchange: Direct (routing key exato), Fanout (broadcast para todos), Topic (pattern matching), Headers (por headers).
Conteúdo
- O que é
- Tipos de Exchange
- Laravel + RabbitMQ
- Direct PHP (sem Laravel)
- Garantias de entrega
- Dead Letter Exchange (DLX)
- Priority Queues
- Monitoramento
- Boas práticas
- Na entrevista
- Exercícios práticos
O que é
RabbitMQ: Message broker para comunicação assíncrona entre serviços. Implementa o protocolo AMQP.
Para quê:
- Processamento assíncrono de tarefas
- Desacoplamento de serviços
- Load leveling (suavizar a carga)
- Garantia de entrega das mensagens
Componentes:
Producer → Exchange → Queue → Consumer
Producer: envia mensagens
Exchange: roteia mensagens para as queues
Queue: guarda mensagens
Consumer: processa mensagensTipos de Exchange
1. Direct Exchange
Princípio: A mensagem vai para a queue com o routing key exato.
Producer → [Direct Exchange] → Queue "emails"
↓ (routing_key: email)
Queue "sms"
(routing_key: sms)Exemplo:
// Producer
$channel->basic_publish(
$message,
'notifications', // exchange
'email' // routing_key
);
// Queue está bound com routing_key = 'email'
// A mensagem cai só nessa queue2. Fanout Exchange
Princípio: Broadcast para todas as queues conectadas.
Producer → [Fanout Exchange] → Queue 1
↓ Queue 2
Queue 3Exemplo:
// Producer
$channel->basic_publish(
$message,
'logs', // fanout exchange
'' // routing_key é ignorado
);
// Todas as queues recebem a mensagemCaso de uso: Logging, monitoramento.
3. Topic Exchange
Princípio: Pattern matching no routing key.
Routing keys:
- user.created
- user.updated
- order.created
- order.shipped
Pattern bindings:
Queue 1: user.* (recebe user.created, user.updated)
Queue 2: order.* (recebe order.created, order.shipped)
Queue 3: *.created (recebe user.created, order.created)
Queue 4: # (recebe tudo)Wildcards:
*— um segmento#— qualquer quantidade de segmentos
Exemplo:
// Producer
$channel->basic_publish(
$message,
'events', // topic exchange
'order.shipped' // routing_key
);
// Queue com pattern 'order.*' recebe a mensagem4. Headers Exchange
Princípio: Routing por headers, não por routing key.
// Producer
$message->set('application_headers', [
'format' => 'pdf',
'type' => 'report'
]);
// Queue está bound com a condição:
// headers: {format: pdf, type: report}Laravel + RabbitMQ
Instalação:
composer require vladimir-yuldashev/laravel-queue-rabbitmqconfig/queue.php:
'connections' => [
'rabbitmq' => [
'driver' => 'rabbitmq',
'queue' => env('RABBITMQ_QUEUE', 'default'),
'connection' => [
'host' => env('RABBITMQ_HOST', '127.0.0.1'),
'port' => env('RABBITMQ_PORT', 5672),
'user' => env('RABBITMQ_USER', 'guest'),
'password' => env('RABBITMQ_PASSWORD', 'guest'),
'vhost' => env('RABBITMQ_VHOST', '/'),
],
'options' => [
'exchange' => [
'name' => 'laravel-exchange',
'type' => 'topic', // direct, fanout, topic, headers
],
],
],
],Job:
class SendEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public $connection = 'rabbitmq';
public $queue = 'emails';
public function __construct(
public string $email,
public string $message
) {}
public function handle()
{
Mail::raw($this->message, function ($mail) {
$mail->to($this->email);
});
}
}
// Dispatch
SendEmailJob::dispatch('joao@email.com', 'Olá!');Worker:
php artisan queue:work rabbitmq --queue=emailsDirect PHP (sem Laravel)
Producer:
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
// Criar exchange
$channel->exchange_declare(
'logs', // exchange name
'fanout', // type
false, // passive
true, // durable
false // auto_delete
);
// Enviar mensagem
$message = new AMQPMessage(
json_encode(['event' => 'user_created', 'user_id' => 123]),
['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
);
$channel->basic_publish($message, 'logs');
$channel->close();
$connection->close();Consumer:
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
// Criar queue
$channel->queue_declare(
'email_queue', // queue name
false, // passive
true, // durable
false, // exclusive
false // auto_delete
);
// Bind da queue no exchange
$channel->queue_bind('email_queue', 'notifications', 'email');
// Callback de processamento
$callback = function ($msg) {
$data = json_decode($msg->body, true);
echo "Processando: {$data['email']}\n";
// Processamento...
// Acknowledge (confirmação)
$msg->ack();
};
// Assinar a queue
$channel->basic_qos(null, 1, null); // Prefetch 1 mensagem
$channel->basic_consume(
'email_queue',
'',
false, // no_ack
false, // exclusive
false, // no_local
false, // no_wait
$callback
);
// Escutar
while ($channel->is_consuming()) {
$channel->wait();
}
$channel->close();
$connection->close();Garantias de entrega
1. Publisher Confirms
Problema: O Producer não sabe se a mensagem chegou.
Solução:
$channel->confirm_select();
$channel->basic_publish($message, 'exchange');
$channel->wait_for_pending_acks(5); // Timeout 5 segundos
// Se der timeout → mensagem não foi entregue2. Consumer Acknowledgments
Manual ACK:
$callback = function ($msg) {
try {
processMessage($msg->body);
// ✅ Sucesso: acknowledge
$msg->ack();
} catch (Exception $e) {
// ❌ Erro: reject e requeue
$msg->nack(false, true); // requeue = true
}
};
$channel->basic_consume('queue', '', false, false, false, false, $callback);
// ↑
// no_ack = false (manual)Auto ACK (perigoso):
// A mensagem some assim que chega no consumer
// Se o consumer cair → mensagem perdida
$channel->basic_consume('queue', '', false, true, ...);
// ↑
// no_ack = true3. Persistent Messages
// Mensagem vai para o disco
$message = new AMQPMessage(
$body,
['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
);
// Queue também precisa ser durable
$channel->queue_declare('queue', false, true, false, false);
// ↑
// durable = trueDead Letter Exchange (DLX)
O que é: Queue para mensagens "mortas" (não processaram depois de N tentativas).
Configuração:
// Queue principal
$channel->queue_declare('emails', false, true, false, false, false, [
'x-dead-letter-exchange' => ['S', 'dlx'],
'x-dead-letter-routing-key' => ['S', 'emails.failed'],
'x-message-ttl' => ['I', 300000], // 5 minutos
]);
// Dead Letter Exchange
$channel->exchange_declare('dlx', 'direct', false, true, false);
// Dead Letter Queue
$channel->queue_declare('emails.failed', false, true, false, false);
$channel->queue_bind('emails.failed', 'dlx', 'emails.failed');Uso:
$callback = function ($msg) {
try {
processEmail($msg->body);
$msg->ack();
} catch (Exception $e) {
// Reject sem requeue → vai para o DLX
$msg->nack(false, false);
}
};Priority Queues
// Queue com prioridades
$channel->queue_declare('tasks', false, true, false, false, false, [
'x-max-priority' => ['I', 10] // Prioridades 0-10
]);
// Enviar com prioridade
$message = new AMQPMessage($body, [
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
'priority' => 5 // Prioridade 5
]);
// Mensagens high priority são processadas primeiroMonitoramento
Management UI:
# Ativar o plugin de management
rabbitmq-plugins enable rabbitmq_management
# http://localhost:15672
# Login: guest / guestCLI:
# Lista de queues
rabbitmqctl list_queues name messages consumers
# Lista de exchanges
rabbitmqctl list_exchanges name type
# Lista de bindings
rabbitmqctl list_bindings
# Status
rabbitmqctl statusBoas práticas
✓ Sempre use durable queues e persistent messages
✓ Manual ACK com try/catch
✓ Dead Letter Exchange para failed messages
✓ Publisher confirms para mensagens críticas
✓ Prefetch limit (1-10) para carga uniforme
✓ Monitoring (queue size, consumer lag)
✓ Idempotency (a mensagem pode chegar duas vezes)
✓ TTL para message expirationNa entrevista
"RabbitMQ é um message broker para comunicação assíncrona. Componentes: Producer, Exchange (direct/fanout/topic), Queue, Consumer. Direct exchange: routing key exato. Fanout: broadcast. Topic: pattern matching (user.*, #). Laravel: vladimir-yuldashev/laravel-queue-rabbitmq, jobs com connection=rabbitmq. Garantias: publisher confirms, manual ACK, persistent messages. DLX para failed messages. Priority queues. Management UI para monitoramento. Boas práticas: durable queues, manual ACK, idempotency."
Exercícios práticos
Exercício 1: Crie um Job para RabbitMQ
Crie um Job que envia email via RabbitMQ com lógica de retry e tratamento de erros.
Solução
// app/Jobs/SendEmailJob.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class SendEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $connection = 'rabbitmq';
public $queue = 'emails';
// Quantidade de tentativas
public $tries = 3;
// Delay entre tentativas (segundos)
public $backoff = [60, 300, 900]; // 1 min, 5 min, 15 min
// Timeout
public $timeout = 120;
public function __construct(
public string $email,
public string $subject,
public string $message
) {}
public function handle(): void
{
Mail::raw($this->message, function ($mail) {
$mail->to($this->email)
->subject($this->subject);
});
Log::info('Email enviado com sucesso', [
'email' => $this->email,
'attempts' => $this->attempts(),
]);
}
public function failed(\Throwable $exception): void
{
Log::error('Falha ao enviar email depois de todas as tentativas', [
'email' => $this->email,
'error' => $exception->getMessage(),
'attempts' => $this->attempts(),
]);
// Pode mandar para o Dead Letter Exchange
// ou avisar o admin
}
}
// config/queue.php
'connections' => [
'rabbitmq' => [
'driver' => 'rabbitmq',
'queue' => env('RABBITMQ_QUEUE', 'default'),
'connection' => [
'host' => env('RABBITMQ_HOST', '127.0.0.1'),
'port' => env('RABBITMQ_PORT', 5672),
'user' => env('RABBITMQ_USER', 'guest'),
'password' => env('RABBITMQ_PASSWORD', 'guest'),
'vhost' => env('RABBITMQ_VHOST', '/'),
],
'options' => [
'exchange' => [
'name' => 'laravel-exchange',
'type' => 'direct',
'declare' => true,
],
'queue' => [
'declare' => true,
'bind' => true,
],
],
],
],
// Uso
SendEmailJob::dispatch('joao@email.com', 'Bem-vindo', 'Olá, mundo!');Exercício 2: Configure um Topic Exchange para eventos
Crie um Topic Exchange para rotear eventos de usuários (user.created, user.updated, user.deleted) para queues diferentes.
Solução
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
class RabbitMQService
{
private $connection;
private $channel;
public function __construct()
{
$this->connection = new AMQPStreamConnection(
'localhost', 5672, 'guest', 'guest'
);
$this->channel = $this->connection->channel();
}
public function setupTopicExchange(): void
{
// Criar Topic Exchange
$this->channel->exchange_declare(
'user_events', // exchange name
'topic', // type
false, // passive
true, // durable
false // auto_delete
);
// Queue para todos os eventos de criação
$this->channel->queue_declare('user_creations', false, true, false, false);
$this->channel->queue_bind('user_creations', 'user_events', '*.created');
// Queue para todos os eventos do usuário com ID 123
$this->channel->queue_declare('user_123_events', false, true, false, false);
$this->channel->queue_bind('user_123_events', 'user_events', 'user.123.*');
// Queue para todos os eventos
$this->channel->queue_declare('all_user_events', false, true, false, false);
$this->channel->queue_bind('all_user_events', 'user_events', 'user.#');
}
public function publishUserEvent(string $routingKey, array $data): void
{
$message = new AMQPMessage(
json_encode($data),
['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
);
$this->channel->basic_publish($message, 'user_events', $routingKey);
}
public function consume(string $queueName, callable $callback): void
{
$this->channel->basic_qos(null, 1, null);
$this->channel->basic_consume(
$queueName,
'',
false, // no_ack
false, // exclusive
false, // no_local
false, // no_wait
function ($msg) use ($callback) {
try {
$data = json_decode($msg->body, true);
$callback($data);
$msg->ack();
} catch (\Exception $e) {
$msg->nack(false, true); // requeue
}
}
);
while ($this->channel->is_consuming()) {
$this->channel->wait();
}
}
public function __destruct()
{
$this->channel->close();
$this->connection->close();
}
}
// Uso
$service = new RabbitMQService();
$service->setupTopicExchange();
// Publicar eventos
$service->publishUserEvent('user.created', ['user_id' => 123, 'email' => 'joao@email.com']);
$service->publishUserEvent('user.123.updated', ['user_id' => 123, 'name' => 'Novo nome']);
// Consumer para todas as criações
$service->consume('user_creations', function ($data) {
echo "Usuário criado: " . $data['user_id'] . "\n";
});Exercício 3: Implemente Dead Letter Exchange
Crie um sistema com Dead Letter Exchange para tratar mensagens failed.
Solução
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Wire\AMQPTable;
class DLXService
{
private $connection;
private $channel;
public function __construct()
{
$this->connection = new AMQPStreamConnection(
'localhost', 5672, 'guest', 'guest'
);
$this->channel = $this->connection->channel();
}
public function setup(): void
{
// Dead Letter Exchange
$this->channel->exchange_declare(
'dlx',
'direct',
false,
true,
false
);
// Dead Letter Queue
$this->channel->queue_declare(
'failed_jobs',
false,
true, // durable
false,
false
);
$this->channel->queue_bind('failed_jobs', 'dlx', 'failed');
// Queue principal com config de DLX
$args = new AMQPTable([
'x-dead-letter-exchange' => 'dlx',
'x-dead-letter-routing-key' => 'failed',
'x-message-ttl' => 300000, // 5 minutos de TTL
]);
$this->channel->queue_declare(
'main_queue',
false,
true,
false,
false,
false,
$args
);
}
public function publishToMainQueue(array $data): void
{
$message = new AMQPMessage(
json_encode($data),
['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
);
$this->channel->basic_publish($message, '', 'main_queue');
}
public function consumeMainQueue(): void
{
$callback = function ($msg) {
$data = json_decode($msg->body, true);
try {
// Simular processamento
if (rand(0, 1) === 0) {
throw new \Exception('Falha no processamento');
}
echo "Processado: " . json_encode($data) . "\n";
$msg->ack();
} catch (\Exception $e) {
echo "Falhou: " . $e->getMessage() . "\n";
// Reject sem requeue → vai para o DLX
$msg->nack(false, false);
}
};
$this->channel->basic_qos(null, 1, null);
$this->channel->basic_consume('main_queue', '', false, false, false, false, $callback);
while ($this->channel->is_consuming()) {
$this->channel->wait();
}
}
public function consumeDeadLetterQueue(): void
{
$callback = function ($msg) {
$data = json_decode($msg->body, true);
echo "Dead letter: " . json_encode($data) . "\n";
// Logar, avisar o admin, gravar no banco
file_put_contents(
'failed_jobs.log',
date('Y-m-d H:i:s') . " - " . $msg->body . "\n",
FILE_APPEND
);
$msg->ack();
};
$this->channel->basic_consume('failed_jobs', '', false, false, false, false, $callback);
while ($this->channel->is_consuming()) {
$this->channel->wait();
}
}
public function __destruct()
{
$this->channel->close();
$this->connection->close();
}
}
// Uso
$service = new DLXService();
$service->setup();
// Publicação
$service->publishToMainQueue(['task' => 'send_email', 'email' => 'joao@email.com']);
// Worker da queue principal
// $service->consumeMainQueue();
// Worker dos failed jobs
// $service->consumeDeadLetterQueue();Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate