Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 13 из 25 ~18 мин +20 XP

Topic: гибкая маршрутизация по шаблону

Иерархии ключей — order.created.eu, user.login.mobile — разруливает topic exchange: привязки те же, что в direct, но сравнение идёт с шаблоном из звёздочек и решёток. В этом уроке — гибкая маршрутизация rabbitmq routing на amqp091-go: синтаксис ключей, правила масок, словарь routing key между командами и цена гибкости. Разберём, почему маска order.* пропускает order.created.eu и когда topic оказывается прямым обменником с лишней нагрузкой.

Содержание урока · 10 разделов

Одиннадцатый урок оставил direct там, где меток немного и все известны. Первые недели хватает: email, push, sms. Потом продюсер хочет различать заказы и платежи, потом города, потом severity — и ключи размножаются: order.email, payment.email, order.email.moscow. Каждая новая грань данных требует новых привязок на стороне всех подписчиков, и рано или поздно кто-то забывает добавить ключ, а потребитель молча перестаёт получать часть потока. Topic exchange решает это сменой правила сравнения: привязка становится шаблоном, и одна маска покрывает целое семейство ключей. Потребитель подписывается на «все события заказов» одной привязкой order.#, продюсер продолжает уточнять ключ сколько угодно — маршрут переживает рост. Промышленные примеры вокруг: нотификации фильтруют user., мониторинг слушает .error, мультирегиональные сервисы режут поток по суффиксу региона. В этом уроке разбираем синтаксис, звёздочки и решётки, ошибки масок и то место, где гибкость превращается в нечитаемую топологию.

Topic exchange и ключ из слов#

Ключ в topic exchange — это слова, разделённые точками: order.created, user.login.mobile, report.sales.eu.weekly. Брокер словарь не навязывает: это строки до 255 байт, точки — разделители, а согласованность слов — контракт между издателями и подписчиками. Хорошая практика: от общего к частному — домен, сущность, действие, уточнения: order.created.eu или order.cancelled.reason.timeout. Обменник объявляется третьим типом из четырёх: ch.ExchangeDeclare("events", "topic", true, false, false, false, nil) — durable, идемпотентность и PRECONDITION_FAILED при смене типа работают как у direct из одиннадцатого урока. Привязка связывает очередь с шаблоном: QueueBind("audit", "#", "events", false, nil). В management UI topic-обменник узнаётся по полю routing key в форме тестовой публикации: вбиваете реальный ключ вроде order.created и видите список очередей, куда ушла копия.

Звезда и решётка: правила шаблонов#

Шаблонов два, и в них путаются, пока не проговоришь правила словами. Звёздочка заменяет ровно одно слово: шаблон order. совпадёт с order.created и order.cancelled, но не с order.created.eu — на месте звёздочки стоит одно слово, а их два. Решётка # заменяет ноль, одно или несколько слов: order.# накрывает order.created, order.created.eu и даже order.cancelled.reason.timeout. Составные шаблоны комбинируют: *.created ловит order.created и user.created, но не report.created.eu. Крайние случаи полезно помнить: одиночный # — это fanout, он пропустит всё; шаблон без звёздочек и решёток — это direct, точное совпадение. То есть topic при желании эмулирует обоих соседей, и в маленьких проектах этим пользуются, чтобы не плодить обменники. Регистр, как и в direct, учитывается: Order.Created и order.created — разные ключи.

Три подписки на один поток событий

GO
package main

import (
        "context"
        "log"
        "os"
        "os/signal"

        amqp "github.com/rabbitmq/amqp091-go"
)

type sub struct {
        queue string
        mask  string
}

func main() {
        conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
        if err != nil {
                log.Fatal(err)
        }
        defer conn.Close()

        ch, err := conn.Channel()
        if err != nil {
                log.Fatal(err)
        }

        err = ch.ExchangeDeclare("events", "topic", true, false, false, false, nil)
        if err != nil {
                log.Fatal(err)
        }

        subs := []sub{
                {queue: "audit.all", mask: "#"},
                {queue: "email.orders", mask: "order.*"},
                {queue: "analytics.users", mask: "user.#"},
        }
        for _, s := range subs {
                if _, err := ch.QueueDeclare(s.queue, true, false, false, false, nil); err != nil {
                        log.Fatal(err)
                }
                if err := ch.QueueBind(s.queue, s.mask, "events", false, nil); err != nil {
                        log.Fatal(err)
                }
        }

        ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
        defer stop()

        msgs, err := ch.ConsumeWithContext(ctx, "email.orders", "email-worker",
                false, false, false, false, nil)
        if err != nil {
                log.Fatal(err)
        }

        for d := range msgs {
                log.Printf("маршрут %s: %s", d.RoutingKey, d.Body)
                if err := d.Ack(false); err != nil {
                        log.Println("ack failed:", err)
                }
        }
}

Три подписки на один поток: аудит забирает всё через решётку, письма — события заказов первого уровня через order.*, аналитика — всё про пользователей через user.#. Delivery несёт RoutingKey — реальный ключ публикации, по нему удобно строить счётчики и логи. Публикация user.login.mobile уйдёт в audit.all и analytics.users и минует email.orders.

Попадания шаблонов: привязка против ключа#

Привязкаorder.createdorder.created.euuser.login
order.*попаданиемимо: два слова на звёздочкемимо
order.#попаданиепопаданиемимо
*.createdпопаданиемимомимо
#попаданиепопаданиепопадание
order.createdпопаданиемимо: точное сравнениемимо

Rabbitmq routing: дизайн ключей и контракт команд#

Гибкость rabbitmq routing перенесла сложность с топологии на словарь: команды договариваются о словах ключа, и без реестра не обойтись. Работающая схема: издатель владеет префиксом своего домена — заказы пишут order., платежи payment., — а подписчики выбирают маски, не спрашивая издателя. Ключ не должен нести данные, которые живут в теле: user.id.1042 создаёт бесконечное множество привязок, идентификатор едёт в payload из пятнадцатого урока. Точки-разделители образуют иерархию, и важно не ссыпать в ключ все грани сразу: user.login.mobile.eu говорит и о канале, и о регионе — при добавлении новой грани чужие маски могут молча сломаться, как в warn-врезке. Словарь меняют аддитивно: новое слово — в конец ключа, старые не переименовывают — вместо этого публикуется новый ключ, и маски переводят постепенно. Delivery.RoutingKey помогает контролю: по нему собирают счётчики потока и ловят незнакомые префиксы. Когда словарь растёт быстрее, чем команды успевают договориться, выручает реестр событий с примерами ключей: он же становится документацией для новичка и входом тестов на маски.

Издатель уточняет ключ, привязки не меняются

GO
ctx := context.Background()
events := []string{
        "order.created",
        "order.created.eu",
        "user.login.mobile",
        "report.sales.weekly",
}
for _, key := range events {
        err := ch.PublishWithContext(ctx, "events", key, false, false,
                amqp.Publishing{
                        ContentType:  "text/plain",
                        Body:         []byte("payload of " + key),
                        DeliveryMode: amqp.Persistent,
                })
        if err != nil {
                log.Fatal(err)
        }
}

Издатель уточняет ключ свободно: грань региона появилась во второй строке, и привязки менять не пришлось — order.# уже её ловит, а order.* честно пропустит. Так и растёт поток событий: уточнение ключей не ломает старых подписчиков, если они выбирали маски, а не точные ключи.

Topic или direct: цена гибкости#

Тема не бесплатна: сопоставление ключа с шаблоном дороже точного сравнения, и брокер выполняет его для каждой привязки на каждое сообщение; на единицах очередей это незаметно, на сотнях активных масок с ветвистыми шаблонами — уже метрика. Direct выигрывает скоростью и читаемостью: множество ключей конечно и видно в UI целиком. Спорное наблюдение из ревью: гибкость topic провоцирует на маски ради масок — привязка вида .paid..error читается хуже, чем честный список ключей direct, и разбор такой топологии превращается в разгадывание. Критерий простой: если потребители выбирают срезы потока с иерархией — topic оправдан; если набор меток стабилен и конечен — direct честнее. Поглощение соседей — ещё одна ловушка: topic умеет всё, но один обменник на все случаи жизни смешивает потоки с разной судьбой, а сменить тип у обменника потом нельзя; чаще выигрывают несколько обменников под разные классы трафика. Проверка простая: выпишите свои маски и подставьте три реальных ключа из продакшн-логов; если половина совпадений мимо или маски дублируют друг друга, топология просит упрощения.

Проверка маршрутизации на живом брокере#

Топологию topic проверяют руками быстрее, чем чтением кода. На странице обменника в management UI есть форма Publish message: вбейте routing key и тело, нажмите publish — и в списке привязок видно, куда ушла копия. Привязки каждой очереди перечислены на её странице в секции Bindings, а консольный rabbitmqctl list_bindings показывает таблицу «обменник — очередь — ключ» целиком: удобно для выгрузки и ревью. На больших схемах заводят подзорную очередь с привязкой # и читают её как дамп потока: видно всё, что публикуется, включая ключи, о которых подписчики не знали. Из той же практики — счётчик по Delivery.RoutingKey на потребителе: внезапно появившийся префикс означает, что издатель расширил словарь, и пора пересматривать маски, пока кто-то не потерял часть потока молча.

Правила topic#

  • Ключ — слова через точки; словарь слов — контракт между издателем и подписчиками.
  • Звёздочка заменяет ровно одно слово, решётка — ноль или больше; решётку оставляют в хвосте шаблона.
  • Одиночный # ведёт себя как fanout, шаблон без масок — как direct.
  • Издатель уточняет ключ свободно, подписчики выбирают маски; новые слова добавляют в конец.
  • Ключ не дублирует payload: идентификаторы и суммы живут в теле сообщения.
  • Гибкость стоит сопоставления на каждой доставке: без иерархии берите direct.

Противовесы этой гибкости: direct из одиннадцатого урока с точным сравнением и fanout из двенадцатого с полной рассылкой — topic их поглощает, но не заменяет по читаемости. Ключи без тела — половина контракта: пятнадцатый урок про JSON расскажет о формате payload, а шестнадцатый про dead letter — куда уходит то, что подписчики не осилили. Дизайн ключей перекликается с потоками Redis: там тоже журнал событий с явными именами потоков. Публикация с ключами осталась в шестом уроке, мёртвые привязки и прочие антипаттерны — в двадцать третьем; карта курса — на странице курса.

Проверка: звезда и решётка

Очередь привязана к topic-обменнику с шаблоном order.*. Какой ключ до неё дойдёт?

Проверка: крайние случаи

К topic-обменнику привязаны две очереди: одна с шаблоном #, вторая — с точным ключом user.login. Публикация с ключом user.login. Что произойдёт?

Сколько доставок получит очередь?

Прочитайте код и выберите, что он выведет:

// topic exchange "events"
// одна очередь reports с двумя привязками: "*.paid" и "order.#"
published := []string{
        "order.paid",
        "order.paid.eu",
        "cart.paid",
        "order.created",
}
fmt.Println("доставок:", countDeliveries(published))

Соберите привязки topic

Допишите объявление topic-обменника и две привязки подписчика.

err := ch.("events", "topic", true, false, false, false, nil) if err != nil { log.Fatal(err) } err = ch.QueueBind("audit.all", , "events", false, nil) if err != nil { log.Fatal(err) } err = ch.QueueBind("email.orders", "order.", "events", false, nil)

Соберите издателя topic

Расставьте строки в порядке выполнения: соединение, канал, обменник, очередь, привязка с маской, публикация.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: topic и маски

Термин

topic exchange

нажмите, чтобы перевернуть
Определение

Обменник с шаблонной маршрутизацией: ключ сравнивается с маской привязки по словам через точки.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Topic сравнивает ключ с шаблоном: звёздочка — одно слово, решётка — хвост любой длины.
  • Одиночный # эквивалентен fanout, точный шаблон — direct: topic покрывает соседей.
  • Словарь ключей — контракт: издатель владеет префиксом домена, подписчики выбирают маски.
  • Новые грани добавляют словами в конец ключа; переименование ломает чужие маски молча.
  • Сообщение с несколькими совпавшими привязками одной очереди приходит один раз.
  • Маски проверяют тестовой публикацией в UI и счётчиком по Delivery.RoutingKey.
  • Без иерархии берите direct: сопоставление шаблонов стоит на каждой доставке.

Домашнее задание

Соберите topic-обменник events и три очереди с привязками #, order.* и user.#. Публикуйте ключи order.created, order.created.eu, user.login.mobile и report.sales.weekly и сверьте в management UI, сколько копий получила каждая очередь. Затем отправьте order.paid.eu и убедитесь, что order.* её пропустил, а после замены маски на order.# подписка догнала поток. В конце заведите подзорную очередь с # и найдите в ней ключ, о котором ваши подписчики не знали.

Личные заметкиПусто
Хранятся только в вашем браузере (RabbitMQ · Topic: гибкая маршрутизация по шаблону)

Вопросы и ответы по уроку

Почему привязка order.* не ловит order.created.eu?

Звёздочка замещает ровно одно слово: на её месте стоит created, а хвосту .eu маски некуда деться. Для ветвящейся иерархии берите решётку order.# — она покрывает и одно, и несколько слов после префикса. Проверить себя быстрее всего тестовой публикацией на странице обменника в management UI.

Придёт ли сообщение дважды, если оно совпало с двумя привязками одной очереди?

Нет: дедупликация работает на пару «очередь и сообщение» — RabbitMQ доставит одну копию независимо от числа совпавших привязок. По копии получат две разные очереди с одинаковыми шаблонами; внутри одной очереди совпадения схлопываются.

Стоит ли завести один topic-обменник на весь проект?

Часто да: один поток событий и общий словарь ключей проще держать в голове, а подписчики выбирают маски сами. Но тип обменника сменить нельзя, и смешивать всё подряд не обязательно: трафик с разной судьбой — задачи воркеров и события для аналитики — живёт лучше в отдельных обменниках со своими требованиями к durable и объёму.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Excelize25 уроков~10 чпрактика25 уроков уже доступны

Excel в Go: чтение и генерация xlsx на excelize

Курс excelize на Go: создание xlsx, чтение, стили, формулы, диаграммы и StreamWriter для больших файлов. 25 уроков с примерами кода и проектом.

  • Создавать и читать xlsx-файлы средствами excelize
  • Работать с листами, ячейками, строками и столбцами
  • Оформлять таблицы: стили, шрифты, границы, форматы чисел и дат
  • Вставлять формулы, диаграммы и изображения