.NET Tutorial
Junior5 мин чтенияВопрос 2 из 9

Разница между очередью (Queue) и топиком (Topic/Exchange)?

Две модели доставки

сообщение 1

сообщение 2

сообщение 3

Producer:
команда SendEmail

Очередь emails

Worker 1

Worker 2

Worker 3

сообщение 1

сообщение 2

сообщение 3

Producer:
команда SendEmail

Очередь emails

Worker 1

Worker 2

Worker 3

Очередь: каждое сообщение — одному потребителю

копия

копия

копия

Producer:
событие OrderPlaced

Топик / exchange
orders.events

подписка billing

Billing

подписка inventory

Inventory

подписка notifications

Notifications

копия

копия

копия

Producer:
событие OrderPlaced

Топик / exchange
orders.events

подписка billing

Billing

подписка inventory

Inventory

подписка notifications

Notifications

Топик: каждое сообщение — каждому подписчику
Очередь Топик
Получателей одного сообщения Один Все подписчики
Отправитель знает получателя Обычно да (адресует очередь) Нет
Тип сообщений Команды, задачи События
Масштабирование Больше потребителей — быстрее разбор Больше подписчиков — больше копий
Если потребителей нет Сообщения копятся Без подписок сообщение теряется (некому доставить)

RabbitMQ: exchanges

Тип exchange Как маршрутизирует Пример
direct Точное совпадение routing key с binding key routingKey = "email" → очередь emails
topic Шаблоны с * (одно слово) и # (0+ слов) order.*.created, order.#
fanout Во все привязанные очереди, ключ игнорируется Broadcast события
headers По заголовкам сообщения x-match: all, region = uz
default ("") В очередь с именем = routing key Простая отправка в очередь

order.*.created ✓

order.# ✓

payment.# ✗

*.uz.* ✓

publish
order.uz.created

topic exchange
events

new-orders

order-audit

payments

uz-analytics

order.*.created ✓

order.# ✓

payment.# ✗

*.uz.* ✓

publish
order.uz.created

topic exchange
events

new-orders

order-audit

payments

uz-analytics

Topic exchange: маршрутизация по шаблонам
Объявление топологии (RabbitMQ.Client)
await ch.ExchangeDeclareAsync("events", ExchangeType.Topic, durable: true);

await ch.QueueDeclareAsync("billing.order-placed", durable: true, exclusive: false, autoDelete: false);
await ch.QueueBindAsync("billing.order-placed", "events", routingKey: "order.placed");

await ch.QueueDeclareAsync("audit.all-orders", durable: true, exclusive: false, autoDelete: false);
await ch.QueueBindAsync("audit.all-orders", "events", routingKey: "order.#");

// Публикация: производитель знает только exchange и тип события
await ch.BasicPublishAsync("events", "order.placed", false, props, body);

Каждый сервис-подписчик создаёт свою очередь и привязывает её к exchange. Несколько экземпляров сервиса читают одну его очередь — получаются competing consumers внутри pub/sub.

Azure Service Bus

C#
// Отправка события в топик
await using var sender = client.CreateSender("orders");
await sender.SendMessageAsync(new ServiceBusMessage(BinaryData.FromObjectAsJson(evt))
{
    MessageId = evt.EventId.ToString(),
    Subject = "OrderPlaced",
    ApplicationProperties = { ["region"] = "uz" }
});

// Каждая подписка имеет фильтр (SQL или correlation)
// subscription "notifications": Subject = 'OrderPlaced'
// subscription "uz-analytics":  region = 'uz'
await using var processor = client.CreateProcessor("orders", "notifications", new ServiceBusProcessorOptions { MaxConcurrentCalls = 10 });

Kafka: consumer groups

Группа analytics — своя копия всех сообщений

экземпляр 1: p0, p1, p2, p3

Группа billing — работает как очередь

экземпляр 1: p0, p1

экземпляр 2: p2, p3

Топик orders: 4 партиции

p0

p1

p2

p3

Группа analytics — своя копия всех сообщений

экземпляр 1: p0, p1, p2, p3

Группа billing — работает как очередь

экземпляр 1: p0, p1

экземпляр 2: p2, p3

Топик orders: 4 партиции

p0

p1

p2

p3

Одна и та же сущность работает как очередь и как pub/sub
  • Внутри группы: каждая партиция — одному потребителю (очередь).
  • Между группами: каждая группа читает все сообщения (pub/sub).
  • Потребителей в группе больше, чем партиций? Лишние простаивают.

Команда в очередь, событие в топик

MassTransit: Send (команда) против Publish (событие)
// Команда — конкретному получателю, в его очередь
var endpoint = await sendEndpointProvider.GetSendEndpoint(new Uri("queue:generate-invoice"));
await endpoint.Send(new GenerateInvoice(orderId));

// Событие — всем, кто подписан на тип сообщения (exchange по типу)
await publishEndpoint.Publish(new OrderPlaced(orderId, customerId, total));

MassTransit на RabbitMQ создаёт exchange на тип сообщения и очередь на endpoint потребителя, связывая их автоматически — Publish превращается в fanout по подписчикам.

Вопросы на засыпку

Что будет с событием, если у топика нет подписчиков?

В RabbitMQ сообщение, не попавшее ни в одну очередь, отбрасывается (или возвращается отправителю с флагом mandatory, или уходит в alternate exchange). В Service Bus сообщение в топике без подходящих подписок не сохраняется. В Kafka сообщение хранится в топике независимо от потребителей — прочитают позже.

Почему не отправить событие в несколько очередей самому?

Тогда отправитель должен знать всех получателей — это жёсткая связанность: новый подписчик требует изменения кода отправителя. Exchange/топик переносят знание о подписчиках в конфигурацию брокера.

Как масштабировать подписчика в pub/sub?

Запустить несколько экземпляров сервиса, читающих одну и ту же его очередь/подписку/consumer group — они станут конкурирующими потребителями, каждое сообщение обработает один экземпляр.

Как в RabbitMQ сделать временного подписчика (например, для WebSocket-уведомлений)?

Очередь с exclusive: true / autoDelete: true и сгенерированным именем — живёт, пока открыто соединение, привязана к fanout/topic exchange. Каждый экземпляр получает все события, пока работает.

Итог

Очередь доставляет каждое сообщение одному из потребителей — модель команд и задач с горизонтальным масштабированием. Топик доставляет копию каждому подписчику — модель событий без знания о получателях. В RabbitMQ это exchange + очереди, в Service Bus — topic + subscriptions, в Kafka — consumer groups; на практике модели комбинируют.