Pub/Sub: рассылка событий в реальном времени

Pub/Sub превращает Redis в брокер сообщений: издатели шлют события в каналы, подписчики мгновенно их получают. Никто никого не знает напрямую.

Издатель кричит в канал, не зная, кто слушает. Подписчики слышат, не зная, кто кричал. Эта развязанность — суть паблиш-сабскрайб.

Publish/Subscribe (Pub/Sub) — модель обмена сообщениями, где отправители (publishers) шлют сообщения в именованные каналы, а получатели (subscribers) подписываются на каналы и получают всё, что туда приходит. Отправители и получатели не знают друг о друге — это слабая связанность.

Команды

# Терминал 1 — подписчик
SUBSCRIBE news
# теперь ждёт сообщений из канала news

# Терминал 2 — издатель
PUBLISH news "Вышла новая версия!"
(integer) 1   # число получивших подписчиков

# Подписка по шаблону
PSUBSCRIBE news.*    # все каналы news.sport, news.tech ...

SUBSCRIBE подписывает на каналы, PUBLISH шлёт сообщение (возвращает число доставленных подписчиков), PSUBSCRIBE подписывает по шаблону с подстановкой.

   Схема Pub/Sub

   Издатель                       Подписчики
   --------                       ----------
   PUBLISH news "..." -->  [канал: news]  --> подписчик A
                                          --> подписчик B
   PUBLISH chat "..." -->  [канал: chat]  --> подписчик C

   Сообщение получают ВСЕ подписчики канала одновременно.
   Кто не подписан в момент отправки — НЕ получит ничего.

Ключевое свойство: fire-and-forget

Pub/Sub доставляет сообщение только тем, кто подписан прямо сейчас. Сообщения не хранятся. Если подписчик отключился — он пропустит всё, что было в это время. Это «выстрелил и забыл»: быстро, но без гарантий доставки.

Демонстрация: простой Pub/Sub на словаре подписок

# Моделируем брокер Pub/Sub: канал -> список подписчиков
channels = {}

def subscribe(channel, name, inbox):
    channels.setdefault(channel, []).append((name, inbox))
    print(f"{name} подписался на '{channel}'")

def publish(channel, message):
    subs = channels.get(channel, [])
    for name, inbox in subs:
        inbox.append(message)        # доставляем КАЖДОМУ подписчику
    print(f"PUBLISH '{channel}': '{message}' -> доставлено {len(subs)}")
    return len(subs)

# Подписчики с личными "ящиками"
alice_inbox, bob_inbox, carol_inbox = [], [], []
subscribe("news", "Alice", alice_inbox)
subscribe("news", "Bob", bob_inbox)
subscribe("chat", "Carol", carol_inbox)

print()
publish("news", "Релиз 2.0!")     # получат Alice и Bob
publish("chat", "Привет!")        # получит только Carol
publish("sport", "Гол!")          # подписчиков нет -> 0

print("\nЯщик Alice:", alice_inbox)
print("Ящик Carol:", carol_inbox)
print("Сообщение 'sport' потеряно — никто не слушал (fire-and-forget).")

Обратите внимание: сообщение в канал sport исчезло бесследно — подписчиков не было. Это и есть природа Pub/Sub: без подписчика в моменте сообщение теряется.

Как работает под капотом

Внутри Redis держит словарь «канал → множество подписавшихся клиентов». При PUBLISH он проходит по этому множеству и проталкивает сообщение в сокет каждого подписчика синхронно. Для PSUBSCRIBE хранится отдельный список шаблонов, которые матчатся при каждой публикации. Никакого хранения, очередей и подтверждений — отсюда и скорость, и отсутствие гарантий.

Частые ошибки

  • Ждать гарантированной доставки. Pub/Sub теряет сообщения для отключённых подписчиков. Нужна надёжность — это Streams (следующий урок).
  • Тяжёлая обработка в подписчике. Медленный подписчик копит буфер на сервере; разгружайте обработку.
  • Использовать для очередей задач. Pub/Sub рассылает всем, а очередь должна отдавать задачу одному воркеру.

Best practices

  • Используйте Pub/Sub для мгновенных уведомлений живым клиентам: чаты, нотификации, обновления дашбордов.
  • Если важна доставка пропущенного — берите Redis Streams.
  • Держите обработчики подписчиков лёгкими; тяжёлую работу выносите в отдельную очередь.

Итог: Pub/Sub — рассылка событий в реальном времени по каналам со слабой связанностью издателей и подписчиков. Это fire-and-forget: быстро, но без хранения и гарантий. Идеален для уведомлений, не подходит для надёжных очередей.

Паттерны на Pub/Sub и его пределы

Несмотря на простоту, Pub/Sub лежит в основе многих реактивных фич. Понимание его сильных и слабых сторон помогает выбрать его осознанно.

  • Живые уведомления. Новое сообщение в чате, обновление статуса заказа, всплывающее оповещение — всё это естественно ложится на Pub/Sub: событие нужно доставить тем, кто онлайн прямо сейчас.
  • Инвалидация кэша между узлами. Один сервер изменил данные и публикует событие «инвалидируй ключ X»; остальные узлы, подписанные на канал, чистят свои локальные кэши.
  • Фан-аут событий. Одно событие нужно разослать многим независимым обработчикам сразу — Pub/Sub делает это одной командой.

Но границы важны не меньше возможностей. Pub/Sub не хранит историю, не гарантирует доставку и не умеет распределять задачу одному воркеру из группы. Если подписчик отключился — он навсегда пропустил то, что было в это время. Поэтому как только появляется требование «не потерять ни одного события» или «обработать каждое ровно одним воркером», нужно переходить на Redis Streams, о которых пойдёт речь дальше. Pub/Sub — про скорость и простоту, Streams — про надёжность.

Проверьте себя
1. Что произойдёт с сообщением PUBLISH, если в момент отправки на канал никто не подписан?
AОно сохранится и будет доставлено первому подписавшемуся
BОно потеряется — Pub/Sub доставляет только активным подписчикам (fire-and-forget)
CRedis вернёт ошибку
DОно встанет в очередь на 60 секунд
2. Почему Pub/Sub не подходит для очереди задач между воркерами?
APub/Sub работает слишком медленно
BPub/Sub рассылает сообщение ВСЕМ подписчикам, а очередь должна отдавать каждую задачу только ОДНОМУ воркеру
CPub/Sub не поддерживает текстовые сообщения
DВ Pub/Sub можно иметь только одного подписчика