RabbitMQ: как работают очереди и надёжная доставка сообщений
Платёж за подписку подтверждён, но доступ клиенту не открылся. Платёжный сервис должен сообщить о событии, сервис подписок проверить своё состояние, а система поддержки увидеть результат. Если все операции выполнять внутри одного HTTP-запроса, ответ платёжного сервиса будет зависеть от скорости и доступности остальных частей системы.
Можно отправить сообщение в RabbitMQ и продолжить обработку позже. Но сразу возникают вопросы. Куда именно попадёт сообщение? Что произойдёт, если сервис подписок выключен? Можно ли считать публикацию успешной после вызова функции publish? Что будет, если обработчик активирует подписку и упадёт до подтверждения задачи?
RabbitMQ помогает передавать работу между приложениями, но надёжность появляется только тогда, когда приложение понимает его модель доставки и правильно обрабатывает сбои. Разберём устройство брокера на одном процессе, а затем соберём рабочую схему публикации и обработки.
Какую проблему решает брокер сообщений
Брокер сообщений принимает данные от одного приложения и передаёт их другим приложениям по заданным правилам. Отправителя называют producer, обработчика - consumer.
В нашем примере платёжный сервис знает, что платёж подтверждён. Ему не обязательно ждать, пока сервис подписок проверит доступ клиента. Он публикует событие billing.payment.confirmed, а подписочный сервис обрабатывает его самостоятельно.
Такое разделение даёт три практических свойства.
Во-первых, отправитель и получатель могут работать с разной скоростью. Если за минуту пришло много платежей, очередь удержит задачи до появления свободных обработчиков. Во-вторых, временная недоступность получателя не обязана останавливать отправителя. В-третьих, одно событие могут независимо использовать несколько систем: подписки, аудит и аналитика.
При этом очередь не делает обработку мгновенной. Она превращает непосредственный вызов в асинхронное взаимодействие: результат появляется позже. Приложению приходится показывать промежуточные состояния, отслеживать задержку и решать, что делать с задачами, которые не удалось выполнить.
Справка. Сообщение в очереди может означать событие или команду. «Платёж подтверждён» описывает уже произошедший факт. «Активировать подписку» просит конкретного исполнителя совершить действие. Для них часто подходят разные маршруты и правила обработки.
Итог раздела:
- RabbitMQ отделяет отправку сообщения от его последующей обработки.
- Вместе с асинхронностью появляются задержки, повторы и необходимость хранить состояние бизнес-процесса.
Путь сообщения: producer, exchange, binding, queue, consumer
В наиболее распространённом для RabbitMQ варианте протокола, AMQP 0-9-1, отправитель публикует сообщение в exchange, а не непосредственно обработчику.
Полный путь выглядит так:
Producer -> Exchange -> Binding -> Queue -> Consumer.
Здесь у каждой части своя задача:
- Producer создаёт сообщение и публикует его.
- Exchange решает, в какие очереди направить сообщение.
- Binding связывает exchange с очередью и задаёт условие маршрутизации.
- Queue удерживает сообщение до доставки обработчику.
- Consumer получает сообщение и выполняет работу.
При публикации producer указывает routing key. Exchange сравнивает его с правилами в bindings. Если сообщение подходит нескольким очередям, каждая получит свою доставку. Сам exchange не является обычным хранилищем задач: хранение происходит в очередях.
Для процесса подписки можно создать billing.events, связать с ним очереди subscriptions.payment-check и billing.audit, а событие публиковать с routing key billing.payment.confirmed. В результате одно событие попадёт в обе очереди. Сервис подписок проверит доступ, а аудит независимо сохранит запись о платеже.
Совсем другая ситуация - два обработчика читают одну очередь subscriptions.payment-check. Они конкурируют за задачи этой очереди: конкретная доставка достанется одному из них. Так можно увеличить производительность проверки платежей, но это не способ раздать копию каждого сообщения обоим обработчикам.
На практике приложение открывает сетевое connection к RabbitMQ, а внутри него работает через один или несколько channels. Канал служит для объявления очередей, публикации и получения сообщений. Кроме того, RabbitMQ использует virtual hosts как пространства имён для exchanges и очередей; на них можно разделять приложения и настраивать права доступа.
Официальная документация RabbitMQ по exchanges подробно описывает маршрутизацию и связи с очередями.
Итог раздела:
- Exchange выбирает очереди, queue удерживает сообщения, consumer выполняет работу.
- Несколько очередей получают независимые копии события; несколько consumers одной очереди распределяют между собой задачи.
Как exchange выбирает очередь
Тип exchange определяет правило маршрутизации. Разберём основные варианты на событиях платёжной системы.
| Тип | Как выбирает очереди | Когда уместен |
|---|---|---|
direct |
Routing key должен точно совпасть с ключом binding | Команды и явно названные маршруты |
fanout |
Передаёт копию во все связанные очереди, игнорируя routing key | Широкое оповещение подписчиков |
topic |
Сопоставляет routing key с шаблоном binding | Группы событий с иерархическими именами |
headers |
Проверяет значения заголовков сообщения | Маршрутизация по нескольким свойствам заголовков |
Для команды subscription.activate подойдёт direct: binding очереди ожидает ключ activate, и отправитель использует именно его.
Для событий удобен topic. Routing key делят точками на части, например billing.payment.confirmed. В шаблоне * заменяет ровно одну часть, а # - ноль или более частей. Поэтому billing.payment.* получит события billing.payment.confirmed и billing.payment.failed, но не billing.payment.refund.completed. Шаблон billing.payment.# охватит и более длинные имена. Если аналитике нужны все события платежей, можно дать ей широкий binding, а сервису подписок - узкий.
fanout подходит, когда выбор по теме не нужен: каждую публикацию получают все подписчики exchange. У него нет смысла тщательно подбирать routing key, поскольку этот тип его игнорирует.
headers полезен для специальных правил по метаданным, но не обязателен для обычного процесса подписки. Если маршрут естественно выражается коротким именем события, direct или topic обычно проще объяснять и сопровождать.
Есть ещё default exchange с пустым именем "". RabbitMQ автоматически связывает с ним каждую объявленную очередь по её имени. Поэтому публикация с routing key, равным имени очереди, выглядит как отправка «прямо в очередь». Внутри всё равно работает exchange. Этот вариант удобен для простых задач, но отдельный exchange делает явной топологию приложения.
Следует помнить и о случае, когда ни один binding не подошёл. Если отправитель не запросил возврат немаршрутизированного сообщения, оно может не попасть ни в одну очередь. Для критичных публикаций это нужно обнаруживать явно.
Итог раздела:
- Routing key задаёт характеристику публикации, binding определяет интерес очереди к этой характеристике.
- Выбор типа exchange выражает смысл доставки: точный маршрут, подписка по теме или рассылка всем.
Что должно быть внутри сообщения
Сообщение лучше делать небольшим и однозначным. Для подтверждённого платежа полезен такой контракт:
{
"event_id": "evt-1048",
"event_type": "billing.payment.confirmed",
"schema_version": 1,
"occurred_at": "2026-10-03T09:10:00Z",
"payment_id": "pay-731",
"subscription_id": "sub-482"
}
event_id позволяет распознавать повторную доставку. event_type и schema_version помогают получателю понять смысл и форму данных. occurred_at отражает время события, а не время его обработки. Идентификаторы платежа и подписки дают возможность получить актуальные сведения из систем, которым принадлежат эти данные.
Не стоит помещать в каждое сообщение полную копию платёжной записи, документы клиента и секреты. Сообщение может попасть в журналы, очередь ошибок или систему мониторинга. При этом и противоположная крайность опасна: если передать только payment_id, а платёжная запись быстро меняется, получатель уже не сможет понять, какое состояние вызвало событие. Нужно решить, какие факты являются частью неизменного события, а какие следует читать заново перед действием.
Контракт сообщения также должен допускать развитие. Если появилось поле currency, старые consumers должны либо корректно игнорировать его, либо получать отдельную версию события. Переименование существующего поля без обновления получателей может сломать обработку, хотя RabbitMQ продолжит исправно доставлять сообщения.
Отдельно полезно передавать trace_id или case_id, чтобы связать публикацию с конкретным обращением. Routing key при этом отвечает за маршрут, а не заменяет полный контракт события.
Итог раздела:
- Сообщение должно иметь устойчивый идентификатор, тип, версию и данные, необходимые получателю.
- Доставка может быть исправной даже при несовместимой схеме сообщения, поэтому контракт проверяют отдельно.
Публикация: запись в сокет ещё не означает доставку
Предположим, платёжный сервис вызвал basic_publish() и функция завершилась. Что именно доказано?
Возможно, клиентская библиотека лишь передала данные в сетевое соединение. Между приложением и брокером может оборваться связь. Exchange может принять сообщение, но не найти подходящей очереди. Поэтому для важного события одной проверки «метод не выбросил исключение» недостаточно.
Publisher confirms позволяют отправителю узнать, что RabbitMQ принял ответственность за опубликованное сообщение. Это подтверждение относится к участку от producer до брокера. Оно не означает, что consumer обработал платёж или активировал подписку. Подтверждения публикации и подтверждения обработки независимы друг от друга. Руководство RabbitMQ по confirms и acknowledgements специально разделяет эти два механизма.
Есть тонкость с маршрутизацией. Брокер способен подтвердить публикацию сообщения, которое не подошло ни одной очереди. Параметр mandatory просит вернуть такое сообщение отправителю. Для критичных событий отправитель должен обрабатывать и publisher confirm, и возврат немаршрутизированного сообщения.
Для сохранения после перезапуска важны свойства очереди и сообщения. В обычной схеме очередь объявляют как durable, а сообщение публикуют как persistent. Но и эти параметры не заменяют publisher confirms: отправителю всё равно нужно знать, принял ли брокер сообщение.
Следующий код показывает саму публикацию через Python-клиент Pika. В рабочей системе событие обычно берут из outbox, который разберём ниже.
import json
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host="localhost")
)
channel = connection.channel()
channel.exchange_declare(
exchange="billing.events",
exchange_type="topic",
durable=True
)
channel.queue_declare(
queue="subscriptions.payment-check",
durable=True,
arguments={"x-queue-type": "quorum"}
)
channel.queue_bind(
exchange="billing.events",
queue="subscriptions.payment-check",
routing_key="billing.payment.confirmed"
)
channel.confirm_delivery()
event = {
"event_id": "evt-1048",
"event_type": "billing.payment.confirmed",
"schema_version": 1,
"payment_id": "pay-731",
"subscription_id": "sub-482"
}
channel.basic_publish(
exchange="billing.events",
routing_key="billing.payment.confirmed",
body=json.dumps(event).encode("utf-8"),
properties=pika.BasicProperties(
content_type="application/json",
delivery_mode=pika.DeliveryMode.Persistent,
message_id=event["event_id"]
),
mandatory=True
)
connection.close()
Здесь объявление exchange, очереди и binding показывает топологию, а confirm_delivery() и mandatory=True защищают разные границы публикации. Конкретное поведение ошибок зависит от клиентской библиотеки; для этого кода их нужно перехватывать и повторять публикацию по правилам приложения.
Итог раздела:
- Publisher confirm подтверждает принятие брокером, а не завершение бизнес-действия.
- Для критичной публикации нужно также обнаруживать отсутствие подходящего маршрута.
Зачем нужна quorum queue
Выбор типа очереди зависит от цены потери сообщения и требуемой доступности.
Classic queue подходит для ряда обычных задач, но в актуальных версиях RabbitMQ не предоставляет репликацию содержимого очереди между узлами кластера. Quorum queue хранит реплики и согласует операции через большинство участников. Если у очереди три реплики, для кворума нужны две. При отказе одного узла оставшиеся могут продолжить работу; при отсутствии большинства доступность операций теряется. Это осознанный обмен части скорости и ресурсов на сохранность данных. Документация RabbitMQ по quorum queues рекомендует их для долгоживущих очередей, где важны репликация и надёжность.
Для событий подтверждённого платежа такой выбор часто оправдан. Потерять сообщение о платеже опаснее, чем потратить дополнительные ресурсы на его запись. Но quorum queue не гарантирует сохранность публикации, которую producer не дождался подтвердить. Сообщение могло ещё не достичь брокера. Поэтому publisher confirms остаются обязательной частью надёжной схемы.
Quorum queue также не является резервной копией бизнес-данных. Если приложение по ошибке подтвердило обработку и удалило задачу из очереди, репликация брокера не восстановит смысл потерянной работы. Источник истины о платеже остаётся в платёжной системе, о подписке - в сервисе подписок, а о ходе обращения - в базе процесса.
Для больших потоков с потребностью перечитывать историю одними и теми же или новыми подписчиками RabbitMQ предлагает streams. Обычная очередь лучше подходит для распределения задач между обработчиками; stream хранит последовательность сообщений для чтения по позиции. Выбор между ними связан с задачей, а не с правилом «новее значит лучше».
Итог раздела:
- Quorum queue защищает содержимое важной очереди при части отказов кластера.
- Репликация не заменяет подтверждение публикации, корректный consumer и собственную базу состояния.
Обработка: когда подтверждать сообщение
RabbitMQ доставляет сообщение consumer. Дальше возможны два режима.
При automatic acknowledgement брокер считает сообщение обработанным после доставки. Если приложение упадёт до записи результата, задача может быть потеряна для процесса.
При manual acknowledgement consumer сам отправляет ack после выполнения нужной работы. Если соединение закроется раньше, неподтверждённое сообщение будет возвращено для повторной доставки. Это делает обработку устойчивее к падению исполнителя, но создаёт возможность дубля. Документация RabbitMQ прямо указывает, что consumers с ручным подтверждением должны быть готовы к повторным доставкам.
Рассмотрим конкретный сбой:
- Consumer получил событие
evt-1048. - Создал в своей базе задачу проверки подписки.
- Упал до отправки
ack. - RabbitMQ выдал
evt-1048другому consumer.
Если второй consumer ещё раз создаст задачу, процесс раздвоится. Поэтому обработчик должен работать идемпотентно: повтор того же события не должен создавать второй логический результат.
Один способ - таблица входящих обработанных событий с уникальным event_id. Создание отметки и задачи происходит в одной транзакции собственной базы:
BEGIN;
INSERT INTO inbox_events (event_id)
VALUES (:event_id)
ON CONFLICT (event_id) DO NOTHING;
INSERT INTO subscription_checks (
source_event_id,
subscription_id,
status
)
VALUES (
:event_id,
:subscription_id,
'PENDING'
)
ON CONFLICT (source_event_id) DO NOTHING;
COMMIT;
После успешного COMMIT consumer отправляет ack. Если он упал до коммита, RabbitMQ доставит сообщение снова, а база не содержит завершённой записи. Если он упал после коммита, но до ack, повторная доставка упрётся в уникальные ограничения и не создаст новую задачу. Это работает, потому что оба INSERT относятся к одной базе и одной транзакции.
Если обработчик сразу вызывает внешний API активации подписки, одной таблицы inbox_events уже недостаточно. Он может вызвать API и упасть до фиксации результата. Для такого действия нужен отдельный устойчивый идентификатор операции, идемпотентный ключ внешнего API или сверка фактического состояния перед повтором. RabbitMQ не может сделать внешнюю операцию однократной сам.
Параметр prefetch ограничивает число доставленных consumer сообщений, которые ещё не подтверждены. Например, prefetch = 10 не позволяет брокеру бесконечно накапливать сообщения у медленного обработчика. Но десять неподтверждённых сообщений не означают, что один последовательный consumer обрабатывает десять задач одновременно.
Итог раздела:
ackотправляют после устойчивого сохранения результата обработки.- Повторная доставка нормальна; защиту от повторного бизнес-действия обеспечивает приложение.
- Prefetch управляет количеством незавершённых доставок, а не гарантирует параллельное исполнение.
Разрыв между базой приложения и RabbitMQ
Предположим, платёжный сервис записал в свою базу payment = confirmed, а затем собирался опубликовать событие. Он упал между этими действиями. Платёж подтверждён, но сервис подписок ничего не узнает.
Обратный порядок тоже плох: событие опубликовано, а транзакция платежа не завершилась. Получатель будет обрабатывать факт, которого нет в исходной базе.
Transactional outbox решает этот разрыв. В одной транзакции платёжный сервис сохраняет новый статус платежа и запись о событии в таблицу outbox. Отдельный отправитель публикует записи outbox в RabbitMQ и отмечает подтверждённые публикации. Этот подход используется для согласования изменения состояния приложения с отправкой событий; пример реализации описан в документации Debezium.
Порядок становится таким:
Изменить платёж и записать outbox в одной транзакции -> опубликовать событие -> дождаться publisher confirm -> отметить запись outbox отправленной.
Но здесь остаётся окно повтора. Отправитель может получить confirm и упасть до отметки outbox. После перезапуска он отправит то же событие снова. Именно поэтому получателю нужен event_id и обработка дублей. Outbox и inbox решают разные половины проблемы.
Справка. В распределённом процессе обещание «ровно один раз» часто скрывает несколько разных гарантий. Сообщение может доставляться повторно, а бизнес-эффект при правильной идемпотентности останется одним. Проверять нужно именно эффект.
Итог раздела:
- Outbox не позволяет забыть событие после изменения своей базы.
- Повторные публикации всё ещё возможны, поэтому consumer должен распознавать их.
Повторы, ошибки и dead-letter exchange
Не всякую ошибку нужно обрабатывать одинаково.
Если сервис подписок временно недоступен, повтор через некоторую задержку может помочь. Если сообщение имеет неизвестную версию схемы, немедленный повтор ничего не изменит. Если платеж относится к другому аккаунту, проблему нужно расследовать, а не бесконечно пробовать активацию.
Consumer может отклонить доставку с requeue = true. Тогда сообщение вернётся в очередь. Но при постоянной ошибке оно способно быстро возвращаться тому же или другому обработчику, создавая цикл, нагрузку и шум. Для временных сбоев лучше задавать ограниченное число попыток с паузами.
Например, приложение может запланировать повторы через 5, 20 и 60 секунд, а затем передать дело сотруднику. Задержку реализуют средствами приложения или топологией очередей. Выбор зависит от версии RabbitMQ, типа очереди и требований к сохранности. В любом варианте число попыток и время следующей попытки должны быть наблюдаемыми.
Dead-letter exchange, или DLX, принимает сообщения, которые были отклонены без возврата в исходную очередь, истекли по TTL, вытеснены ограничением длины очереди или превысили допустимое число доставок для quorum queue. DLX маршрутизирует их дальше, например в очередь ручного разбора. Это не «корзина, которая сама исправляет ошибки»: кому-то нужно наблюдать за ней и разбирать причины. Условия dead-lettering описаны в официальном руководстве RabbitMQ.
Есть важный нюанс надёжности: перемещение через DLX является новой публикацией. В обычной конфигурации при проблеме целевой очереди оно не обладает той же гарантией, которую разработчик мог ожидать от исходного сообщения. Для quorum queues существует специально настраиваемый режим dead-lettering с гарантией как минимум одной доставки. Если потеря сообщения при переносе недопустима, его настройки нужно проверить отдельно, а не полагаться на само наличие DLX.
Для нашего обращения очередь ручного разбора должна содержать ссылку на case_id и причину остановки. Состояние самого дела также переводят в NEEDS_ATTENTION. Тогда обращение видно сотруднику, даже если он не смотрит непосредственно в RabbitMQ.
Итог раздела:
- Временные и постоянные ошибки требуют разных маршрутов.
- Немедленный
requeueспособен создать бесконечный цикл. - DLX требует явной настройки сохранности и операционного процесса разбора.
Порядок сообщений и параллельность
Название «очередь» создаёт ожидание строгого порядка. Но в распределённой обработке нужно уточнять, какого именно порядка мы хотим.
Сообщения могут поступить в очередь в определённой последовательности, но при нескольких consumers завершиться в другом порядке. Первый обработчик получил событие раньше, затем долго ждал платёжный API; второй получил следующее событие и быстро завершил работу. Повторная доставка и отдельные маршруты ещё сильнее усложняют картину.
Допустим, для подписки sub-482 пришли события subscription.activated и subscription.cancelled. Если второе обработалось раньше первого, нельзя просто установить статус по последнему обработанному событию. В сообщение полезно включать версию объекта или порядковый номер изменения, а в базе применять только допустимые переходы. Например, изменение с версией 8 не должно перезаписывать состояние версии 9.
Когда нужен действительно строгий последовательный обработчик для одного ключа, архитектуру подбирают специально: ограничивают параллелизм, разделяют поток по ключу или используют средство с подходящими гарантиями порядка. Это может уменьшить общую пропускную способность, поэтому требование порядка формулируют по конкретному объекту, например «все изменения одной подписки», а не автоматически по всему потоку платежей.
Итог раздела:
- Порядок получения, порядок завершения и порядок бизнес-изменений не совпадают автоматически.
- Для критичного состояния нужны версии и проверка допустимых переходов.
Как рассчитать нагрузку и не перегрузить обработчики
Пусть один последовательный consumer тратит в среднем 0,2 секунды на проверку подписки. Его приблизительная производительность равна 1 / 0,2 = 5 сообщений в секунду. Пять одинаковых consumers при отсутствии других ограничений обработают примерно 5 × 5 = 25 сообщений в секунду.
Если приходит 30 сообщений в секунду, накопление составит примерно 30 − 25 = 5 сообщений в секунду. За минуту очередь вырастет приблизительно на 5 × 60 = 300 сообщений.
Это упрощённая оценка. Реальную скорость ограничивают платёжный API, база данных, сеть, размер сообщений и распределение времени обработки. Но она показывает важное свойство: очередь сглаживает временный всплеск, а не создаёт вычислительную мощность. При постоянном превышении входящего потока над скоростью обработки задержка будет расти.
Увеличение prefetch с 10 до 100 само по себе не ускорит последовательный обработчик. Оно лишь позволит передать ему больше сообщений до подтверждения предыдущих. При слишком большом значении часть задач может долго находиться у медленного consumer, а не распределяться между свободными.
В эксплуатации следует наблюдать отдельно:
- ready - сколько сообщений ждёт доставки;
- unacknowledged - сколько уже выдано, но ещё не подтверждено;
- скорость публикации и завершения обработки;
- число consumers и длительность обработки;
- сообщения в очереди ошибок;
- свободное место на диске, память и состояние узлов.
Если растёт ready, обработчики могут не успевать или отсутствовать. Если растёт unacknowledged, они получают задачи, но долго не подтверждают их: возможно, завис внешний API. Если очередь пуста, это всё равно не доказывает, что бизнес-процесс завершён: сообщения могли быть подтверждены раньше фактического действия. Руководство RabbitMQ по мониторингу подчёркивает необходимость сочетать метрики брокера с метриками приложений.
Итог раздела:
- Пропускная способность зависит от реального времени работы обработчиков и их числа.
- Метрики очереди показывают движение сообщений; итог подписки проверяют в бизнес-системе.
Рабочая схема для обращения о подписке
Теперь соберём механизм целиком.
Платёжный сервис фиксирует подтверждение платежа и outbox-событие в одной транзакции. Отправитель outbox публикует billing.payment.confirmed в billing.events с publisher confirms и контролем маршрута. Topic exchange направляет сообщение в очередь проверки подписки и отдельно в очередь аудита.
Consumer проверки получает сообщение с ручным подтверждением. В своей базе он по event_id один раз создаёт задачу проверки и только затем отправляет ack. Исполнитель читает актуальные данные платёжной и подписочной систем. Если платёж подтверждён, а доступ закрыт, процесс готовит предложение о восстановлении и переводит обращение в ожидание сотрудника.
После одобрения приложение снова проверяет критичные факты. Оно создаёт устойчивую запись действия с собственным action_id и публикует команду subscription.activate через отдельный direct exchange. Исполнитель вызывает сервис подписок с тем же идентификатором действия при каждом повторе. После тайм-аута он выясняет фактический результат, а не отправляет новую независимую команду. Клиенту сообщают об открытом доступе только после подтверждения состояния подписки.
В этой схеме RabbitMQ отвечает за маршрут и доставку сообщений. Базы приложений отвечают за состояние платежа, подписки и обращения. Код обработчиков отвечает за идемпотентность, права и допустимые переходы. Такое разделение позволяет понять, какой компонент следует исправлять при каждом сбое.
Когда RabbitMQ подходит, а когда он усложнит задачу
RabbitMQ полезен, когда работу можно выполнить позже, отправитель и получатель должны быть развязаны по времени, задачи нужно распределять между обработчиками или одно событие используют несколько систем.
Если одному сервису нужен немедленный ответ другого и без него нельзя продолжить текущий запрос, прямой HTTP-вызов может быть проще. Поместить запрос в RabbitMQ, а затем синхронно ждать ответ из очереди иногда возможно, но это не устраняет ожидание и добавляет инфраструктуру.
Если основная задача - долго хранить большой поток событий для многократного чтения разными подписчиками, стоит рассмотреть streams или специализированный журнал событий. Если же нужен управляемый поток заданий с гибкой маршрутизацией, RabbitMQ и обычные очереди соответствуют задаче лучше.
Главное правило применения: сообщение считается надёжно обработанным только тогда, когда приложение устойчиво сохранило результат и может безопасно пережить повторную доставку. Exchange, quorum queue, publisher confirm и consumer ack помогают на разных участках пути. Ни один из них по отдельности не гарантирует, что подписка клиента действительно открылась.