RabbitMQ
Что такое RabbitMQ
RabbitMQ — брокер сообщений с расширенной маршрутизацией.
Отличия от Redis:
- Надёжность — сообщения сохраняются на диск.
- Гарантии — подтверждения доставки.
- Маршрутизация — exchanges, routing keys.
- Dead letter — встроенная очередь для failed.
- Приоритеты — на уровне очереди.
Когда Redis лучше:
- Простые очереди.
- Максимальная скорость.
- Не нужна сложная маршрутизация.
Когда RabbitMQ лучше:
- Критичные сообщения.
- Сложная маршрутизация.
- RPC между сервисами.
- Микросервисы.
Установка
Docker:
docker run -d --name rabbitmq \
-p 5672:5672 -p 15672:15672 \
rabbitmq:3-management
URL UI: http://localhost:15672 (guest/guest).
Основные концепции
Producer — отправляет сообщение в exchange.
Exchange — маршрутизирует в очереди.
Queue — хранит сообщения.
Consumer — читает из очереди.
Binding — связь exchange → queue.
Producer → Exchange → [Binding] → Queue → Consumer
Типы Exchange
Direct — точное совпадение routing key:
exchange: orders
exchange orders → routing key 'order.created' → queue order-emails
Fanout — всем очередям:
exchange logs → queue analytics + queue audit + queue alerts
Topic — по шаблону:
exchange events → 'user.*' → queue user-events
exchange events → '*.created' → queue created-events
Headers — по заголовкам (редко).
Установка клиента
Node.js:
npm install amqplib
PHP:
composer require php-amqplib/php-amqplib
Producer (Node.js)
import amqp from 'amqplib';
const conn = await amqp.connect('amqp://guest:guest@localhost:5672');
const channel = await conn.createChannel();
const exchange = 'orders';
await channel.assertExchange(exchange, 'topic', { durable: true });
channel.publish(
exchange,
'order.created',
Buffer.from(JSON.stringify({ orderId: 123, total: 500 })),
{ persistent: true },
);
console.log('Sent');
Что здесь:
assertExchange— создать если нет.durable: true— переживёт рестарт.persistent: true— сохранится на диск.
Consumer (Node.js)
const conn = await amqp.connect('amqp://guest:guest@localhost:5672');
const channel = await conn.createChannel();
const exchange = 'orders';
const queue = 'order-emails';
await channel.assertExchange(exchange, 'topic', { durable: true });
await channel.assertQueue(queue, { durable: true });
await channel.bindQueue(queue, exchange, 'order.created');
channel.prefetch(10); // по 10 сообщений за раз
channel.consume(queue, async (msg) => {
if (!msg) return;
try {
const data = JSON.parse(msg.content.toString());
await sendOrderEmail(data);
channel.ack(msg); // подтверждение
} catch (err) {
console.error(err);
channel.nack(msg, false, true); // вернуть в очередь
}
}, { noAck: false });
Что здесь:
prefetch(10)— не более 10 неподтверждённых.ack— успех.nack— вернуть или отбросить.
Fanout — всем
await channel.assertExchange('logs', 'fanout', { durable: true });
// Три очереди
await channel.assertQueue('logs:analytics', { durable: true });
await channel.assertQueue('logs:alerts', { durable: true });
// Без routing key
await channel.bindQueue('logs:analytics', 'logs', '');
await channel.bindQueue('logs:alerts', 'logs', '');
// Одно сообщение — всем
channel.publish('logs', '', Buffer.from('user registered'));
Что даёт: каждое сообщение — всем подписчикам.
Topic — по шаблону
await channel.assertExchange('events', 'topic', { durable: true });
await channel.assertQueue('user-events');
await channel.bindQueue('user-events', 'events', 'user.*');
await channel.assertQueue('all-created');
await channel.bindQueue('all-created', 'events', '*.created');
// Матчится user.*
channel.publish('events', 'user.created', ...);
// Матчится обоими
channel.publish('events', 'user.created', ...);
Шаблоны:
*— одно слово.#— ноль или больше слов.user.*—user.created,user.deleted.order.#—order,order.created,order.created.paid.
RPC — запрос-ответ
Клиент:
const correlationId = crypto.randomUUID();
const { queue } = await channel.assertQueue('', { exclusive: true });
channel.sendToQueue(
'rpc_queue',
Buffer.from(JSON.stringify({ userId: 1 })),
{ correlationId, replyTo: queue },
);
channel.consume(queue, (msg) => {
if (msg?.properties.correlationId === correlationId) {
const result = JSON.parse(msg.content.toString());
channel.ack(msg);
console.log('Result:', result);
}
}, { noAck: false });
Сервер:
channel.consume('rpc_queue', async (msg) => {
if (!msg) return;
const { userId } = JSON.parse(msg.content.toString());
const result = await getUserTariff(userId);
channel.sendToQueue(
msg.properties.replyTo,
Buffer.from(JSON.stringify(result)),
{ correlationId: msg.properties.correlationId },
);
channel.ack(msg);
});
Что даёт: синхронный вызов между сервисами через очередь.
Dead Letter Queue
Очередь с DLX:
await channel.assertQueue('orders', {
durable: true,
deadLetterExchange: 'orders.dlx',
deadLetterRoutingKey: 'failed',
});
await channel.assertExchange('orders.dlx', 'direct', { durable: true });
await channel.assertQueue('orders.failed', { durable: true });
await channel.bindQueue('orders.failed', 'orders.dlx', 'failed');
Что даёт: все nack/reject идут в orders.failed.
TTL — время жизни
На сообщение:
channel.publish(exchange, routingKey, content, {
expiration: '60000', // 60 сек
});
На очередь:
await channel.assertQueue('temp', {
durable: false,
arguments: { 'x-message-ttl': 60000 },
});
Правило: expired сообщения → DLQ.
Приоритеты
await channel.assertQueue('tasks', {
durable: true,
maxPriority: 10,
});
channel.publish(exchange, key, content, { priority: 9 }); // важнее
channel.publish(exchange, key, content, { priority: 1 }); // обычное
Что даёт: приоритетные вперёд.
Lazy queues
Проблема: миллион сообщений жрёт RAM.
Решение: lazy — хранятся на диск.
await channel.assertQueue('bulk', {
durable: true,
arguments: { 'x-queue-mode': 'lazy' },
});
Что даёт: RAM почти не тратится.
Отличие от Laravel Queue
Laravel по умолчанию: Redis / database.
Laravel + RabbitMQ:
composer require vyuldashev/laravel-queue-rabbitmq
QUEUE_CONNECTION=rabbitmq
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
RABBITMQ_QUEUE=default
// config/queue.php
'rabbitmq' => [
'driver' => 'rabbitmq',
'host' => env('RABBITMQ_HOST'),
'port' => env('RABBITMQ_PORT'),
'vhost' => env('RABBITMQ_VHOST', '/'),
'login' => env('RABBITMQ_LOGIN'),
'password' => env('RABBITMQ_PASSWORD'),
'queue' => env('RABBITMQ_QUEUE', 'default'),
'options' => [
'queue' => [
'exchange' => 'laravel',
'exchange_type' => 'topic',
'queue_max_priority' => 10,
],
],
],
Dispatch как обычно:
SendEmailJob::dispatch($user)->onQueue('emails');
Межъязыковой обмен
PHP → Node.js:
// Laravel публикует
$this->amqp->publish(
'events',
'user.created',
json_encode(['userId' => $user->id]),
);
// Node.js читает
channel.consume('user-events', (msg) => {
const { userId } = JSON.parse(msg.content.toString());
// ...
});
Правило: JSON — универсальный формат.
Мониторинг
Management UI:
- Queues — размер, consumers.
- Messages — publish / deliver / ack.
- Connections — кто подключён.
- Channels — открытые каналы.
Метрики:
- Messages ready — сколько ждёт.
- Messages unacked — сколько в работе.
- Message rate — сколько в секунду.
- Consumer count — живые воркеры.
Практика: система событий
1. Exchange:
await channel.assertExchange('app-events', 'topic', { durable: true });
2. Producer (Laravel):
public function publish(string $event, array $data): void
{
$this->amqp->publish('app-events', $event, json_encode($data));
}
// Использование
$this->publish('user.registered', ['userId' => $user->id]);
$this->publish('order.created', ['orderId' => $order->id]);
3. Consumers (Node.js):
// Аналитика
await channel.assertQueue('analytics');
await channel.bindQueue('analytics', 'app-events', '#'); // всё
// Email
await channel.assertQueue('email-service');
await channel.bindQueue('email-service', 'app-events', 'user.*');
await channel.bindQueue('email-service', 'app-events', 'order.created');
4. Обработка:
channel.consume('email-service', async (msg) => {
const event = msg.fields.routingKey;
const data = JSON.parse(msg.content.toString());
switch (event) {
case 'user.registered':
await sendWelcome(data.userId);
break;
case 'order.created':
await sendOrderConfirmation(data.orderId);
break;
}
channel.ack(msg);
});
Частые ошибки
1. Забыли durable: true:
После рестарта очереди исчезли
2. Сообщения не persistent:
После рестарта задачи потерялись
Решение: { persistent: true } при publish.
3. Забыли ack:
Unacked растёт, память кончается
Решение: channel.ack(msg) всегда.
4. Нет prefetch:
Один воркер забирает все сообщения
Решение: channel.prefetch(10).
5. Consumer падает без nack:
Сообщение навсегда unacked
Решение: обернуть в try/catch + nack.
6. Один exchange на всё:
Маршрутизация превращается в кашу
Решение: по одному exchange на домен (orders, users, logs).
Итог
- RabbitMQ — надёжный брокер с маршрутизацией.
- Exchange типы: direct, fanout, topic, headers.
- Producer → exchange → binding → queue → Consumer.
ack/nack— подтверждения.prefetch— контроль нагрузки.- DLX — Dead Letter.
- RPC — запрос-ответ через очередь.
- Laravel — пакет
laravel-queue-rabbitmq. - Management UI —
:15672.
Дальше — real-time уведомления.