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
- Redis Documentation: Pub/Sub. https://redis.io/docs/interact/pubsub/
- Redis Documentation: Sharded Pub/Sub. https://redis.io/docs/interact/pubsub/#sharded-pubsub