Pub/Sub

Предпосылки: Event loop, List (для сравнения), TCP (соединение). Секция Sharded Pub/Sub использует понятия из Redis Cluster.

Bitmap и Bitfield | Атомарность одной команды

Проблема broadcast’а

Инвалидация кеша. 8 серверов приложений, каждый держит локальный кеш цен товаров. Сервер 3 обновил цену product:42 в базе. У остальных 7 — устаревший кеш. Как сообщить всем одновременно? Не нужна персистентность — если сервер был выключен, он получит свежие данные при следующем cache miss. Не нужно подтверждение — отправитель не ждёт ответа. Нужно, чтобы все подключённые серверы услышали «invalidate product:42» прямо сейчас. Это broadcast — и именно его обеспечивает Pub/Sub. Pub/Sub решает проблему когерентности локального кэша через event-based инвалидацию.

PUBLISH и SUBSCRIBE

Pub/Sub работает через пару операций. Подписчик регистрируется на канал, отправитель публикует в канал:

-- подписчик (в отдельном соединении):
SUBSCRIBE cache:invalidation
-- соединение переходит в режим подписки
-- клиент получает сообщения по мере поступления
 
-- отправитель (в другом соединении):
PUBLISH cache:invalidation '{"key":"product:42","action":"invalidate"}'
-- → 3 (количество подписчиков, получивших сообщение)

PUBLISH возвращает число подписчиков, получивших сообщение. Если 0 — никто не слушает, сообщение потеряно. PSUBSCRIBE подписывает на шаблон: PSUBSCRIBE cache:* получит сообщения из всех каналов, начинающихся с cache:.

SUBSCRIBE переводит соединение в особый режим — и это имеет важные последствия.

Выделенное соединение

В режиме подписки соединение принимает только команды подписки (SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PING, RESET). Никакие другие команды — GET, SET, INCR — выполнить нельзя. Redis вернёт ошибку. Для обычных команд нужно отдельное TCP-соединение.

Каждый подписчик = одно TCP-соединение, которое удерживается на всё время подписки. При 10 000 подписчиков — 10 000 открытых соединений к Redis. Это не проблема для десятков серверов приложений, каждый из которых держит одно-два соединения для подписки. Но для архитектуры, где каждый пользовательский WebSocket (постоянное двустороннее соединение между браузером и сервером) — отдельная подписка, число соединений может быстро исчерпать лимит maxclients (по умолчанию 10 000): новые подключения начнут получать отказ.

Fire-and-forget как осознанный выбор

Pub/Sub не хранит сообщения. Если в момент публикации подписчик отключён — сообщение потеряно. Нет истории, нет retry, нет подтверждения доставки.

Это не ограничение — это свойство, которое делает Pub/Sub самым быстрым механизмом доставки в Redis. Нет хранения — нет роста памяти, нет необходимости в обрезке и очистке. Нет подтверждений — нет overhead’а отслеживания pending-сообщений. Минимальная работа сервера на каждое сообщение — только скопировать данные в буферы подписчиков. Результат: наименьшая латентность доставки среди всех механизмов Redis.

Когда нужна гарантия доставки и возможность повторного чтения — Stream. Pub/Sub и Stream не конкурируют — они решают разные задачи.

Sharded Pub/Sub в кластере

В Redis Cluster классический PUBLISH рассылает сообщение на ВСЕ узлы кластера, даже если подписчики есть только на одном. В кластере из 30 узлов каждое сообщение создаёт 30 копий — 29 из которых могут быть бесполезны. При тысячах сообщений в секунду это создаёт ощутимый лишний трафик между узлами.

Sharded Pub/Sub (Redis 7.0+, команды SSUBSCRIBE, SUNSUBSCRIBE, SPUBLISH) привязывает канал к конкретному слоту по хешу имени канала. Сообщение обрабатывается только узлом, владеющим этим слотом, и доставляется только подписчикам этого узла. Подписчики подключаются к нужному узлу, отправители публикуют в нужный узел — трафик локализован. В том же 30-узловом кластере sharded Pub/Sub устраняет 29/30 лишнего трафика.

LIST, Pub/Sub или Stream

Один получатель, сообщение можно забрать и потерять — LIST. BRPOP доставляет элемент одному клиенту, и после этого элемент исчезает. Повторное чтение невозможно. Sidekiq и другие обработчики задач используют именно этот паттерн — если задача провалилась, приложение ставит её заново.

Все подписчики одновременно, история не нужна — Pub/Sub. Инвалидация кеша, обновления статуса, координация WebSocket’ов между серверами. Если подписчик отсутствовал — ничего страшного, он получит актуальные данные другим способом.

Все подписчики или один в группе, с историей и подтверждением — Stream. Аудит-логи, потоки событий, очереди с гарантией обработки. Каждое сообщение хранится, можно перечитать по ID, consumer groups распределяют нагрузку, XACK подтверждает обработку.

См. также

  • Sub в Rails — PUBLISH/SUBSCRIBE, fire-and-forget
  • Sub — координация WebSocket’ов в Rails

Sources


Bitmap и Bitfield | Атомарность одной команды