Сервис заказов публикует события, а получателей уже несколько: письма, склад, аналитика. У каждого свои интересы: письмам нужны только оплаты, склад работает с одним регионом, аналитике нужно всё. Кто решает, какое сообщение в какую очередь попадёт? Это работа exchange. В этом уроке разберём его типы — direct, fanout, topic и headers, научимся строить подписки, а потом посмотрим, что делать, когда обработка сообщения не удаётся: для этого в RabbitMQ есть dead-letter очереди, TTL и повторы с задержкой.

Как exchange принимает решение

Вспомним цепочку: producer → exchange → binding → очередь → consumer. Exchange ничего не хранит: он смотрит на ключ маршрутизации (routing key) сообщения и на привязки (binding) и копирует сообщение в подходящие очереди. Привязка — это пара «очередь + ключ привязки» (binding key). Тип exchange определяет, как сравниваются эти два ключа:

ТипПравилоПример
directключи совпадают точноуровень лога: error
fanoutключ игнорируется, копия уходит всемсобытие для всех сервисов
topicключ сопоставляется с шаблономorder.*.paid
headersсравниваются заголовки, а не ключformat=pdf
Direct маршрутизирует по точному ключу и отбрасывает сообщение без привязки; fanout копирует сообщение во все очереди
Direct маршрутизирует по точному ключу и отбрасывает сообщение без привязки; fanout копирует сообщение во все очереди

direct: точное совпадение

Пример — логи по уровням. Ошибки (error) нужны очереди оповещений, а info и warning — консоли:

javascript
import amqp from 'amqplib';

const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();

await channel.assertExchange('logs', 'direct', { durable: true });
await channel.assertQueue('alerts', { durable: true });
await channel.assertQueue('console', { durable: true });

await channel.bindQueue('alerts', 'logs', 'error');
await channel.bindQueue('console', 'logs', 'info');
await channel.bindQueue('console', 'logs', 'warning');

const send = (level, text) =>
  channel.publish('logs', level, Buffer.from(text));

send('error', 'Payment service is down');
send('info', 'Order 7 created');
send('trace', 'nobody listens to this level');

Сообщение с ключом error попадёт в alerts, info — в console. А trace, для которого нет привязки, молча пропадёт: exchange не знает, куда его отправить, и отбрасывает. Чтобы замечать такие потери, у publish есть флаг mandatory, а у exchange — настройка alternate exchange.

💡 Если привязать к одному ключу несколько очередей, каждая получит свою копию: direct умеет и это.

fanout: копия всем

fanout игнорирует ключ и копирует каждое сообщение во все привязанные очереди. Это самый простой pub/sub из первого урока модуля. Подписчик объявляет свою очередь и привязывает её к exchange: