Real-Time Backend

Redis Pub/Sub

Приложение работает на одном сервере - всё хорошо. Запускают второй сервер для масштабирования - и половина пользователей перестаёт получать сообщения друг от друга. Redis Pub/Sub - стандартное решение этой проблемы.

  • **Socket.io Redis Adapter** используется в production-приложениях для синхронизации WebSocket-событий между узлами кластера - `io.emit()` на любом сервере доходит до всех клиентов
  • **Slack** применял Redis Pub/Sub для распределённого presence: событие `user-online` публикуется в канал, все серверы команды мгновенно обновляют статус участника
  • **Инвалидация кеша**: при изменении данных один сервис публикует событие `cache:invalidate:product-42`, все остальные узлы сбрасывают свой локальный кеш без прямых вызовов между сервисами

Как работает Redis Pub/Sub

Redis Pub/Sub - это механизм обмена сообщениями по модели публикация-подписка. Один процесс публикует сообщение в **канал**, все подписанные процессы получают его немедленно. Redis выступает брокером: он не хранит историю и не гарантирует доставку - сообщение живёт ровно столько, сколько нужно передать подписчикам.

Ключевое свойство: **эфемерность**. Если подписчик не подключён в момент публикации - сообщение теряется навсегда. Pub/Sub - это шина событий, а не очередь с хранилищем.

  • **SUBSCRIBE channel** - подписаться на конкретный канал
  • **PUBLISH channel message** - опубликовать сообщение
  • **UNSUBSCRIBE channel** - отписаться
  • **PSUBSCRIBE pattern** - подписка по wildcard-паттерну (`chat:*`)

Сервер A публикует событие в Redis-канал. Сервер B в этот момент перезагружается. Что произойдёт с событием?

Проблема нескольких WebSocket-серверов

Сценарий: Socket.io приложение запущено на двух серверах за балансировщиком. Пользователь Alice подключена к серверу-1, Bob - к серверу-2. Когда сервер-1 делает `io.emit('message', data)` - сообщение уходит только клиентам сервера-1. Bob не получит ничего.

Это классическая проблема горизонтального масштабирования stateful-серверов. Каждый WebSocket-сервер знает только о своих соединениях. Нужен общий канал синхронизации между узлами - Redis Pub/Sub идеально подходит для этой роли.

Socket.io кластер из 3 серверов. `socket.to('room-X').emit('update')` вызван на сервере-1. Клиент в room-X подключён к серверу-3. Что нужно для корректной доставки?

Socket.io Redis Adapter

`@socket.io/redis-adapter` - официальный адаптер, который заменяет встроенный in-memory адаптер Socket.io. При broadcast-операциях он публикует событие в Redis, каждый сервер подписан на общий канал и доставляет событие своим локальным клиентам.

Два отдельных соединения обязательны: Redis не позволяет использовать одно соединение одновременно для PUBLISH и SUBSCRIBE - клиент в режиме подписки принимает только команды SUB/UNSUB.

Slack использовал Redis Pub/Sub для синхронизации presence-статусов между серверами: когда пользователь заходит в систему, событие публикуется в канал `presence:user-id`, и все узлы, обслуживающие его команду, обновляют отображение статуса в реальном времени.

  • **io.emit()** - все клиенты всех узлов
  • **io.to('room').emit()** - все клиенты комнаты на всех узлах
  • **socket.broadcast.emit()** - все кроме отправителя, на всех узлах
  • **socket.emit()** - только один клиент, адаптер не нужен

Почему для Redis Adapter нужно два отдельных Redis-соединения - pubClient и subClient?

Паттерны каналов и ограничения

Redis Pub/Sub поддерживает **wildcard-подписки** через `PSUBSCRIBE`. Паттерн `chat:*` подпишет на все каналы вида `chat:room-1`, `chat:room-42` и т.д. Это удобно для маршрутизации событий по пространствам имён без явного перечисления каналов.

Ключевые ограничения Redis Pub/Sub, которые определяют когда его использовать, а когда нет:

  • **Нет персистентности** - сообщения не сохраняются, опоздавший подписчик их не получит
  • **Нет подтверждения доставки** - нельзя узнать, получили ли подписчики сообщение
  • **Нет фильтрации на стороне Redis** - все подписчики получают все сообщения канала
  • **Нет приоритетов** - все сообщения равнозначны
  • **Для очередей** - использовать Redis Streams или BullMQ поверх List

Redis Pub/Sub идеален для **fire-and-forget** сценариев: синхронизация кешей между узлами, рассылка событий присутствия, инвалидация CDN. Для надёжной доставки с гарантиями - Redis Streams (с `XREAD` и consumer groups).

Redis Pub/Sub и Redis Streams - это одно и то же, просто разные API

Pub/Sub - эфемерная шина событий без хранилища. Streams - персистентный лог с consumer groups и подтверждением доставки

Pub/Sub сообщение живёт миллисекунды и исчезает после доставки. Stream-запись хранится до явного удаления и может быть прочитана повторно - это принципиально разные гарантии надёжности

Система уведомлений: пользователь должен получить push-уведомление ровно один раз, даже если был оффлайн. Redis Pub/Sub подходит?

Итоги

  • **Redis Pub/Sub эфемерен**: нет персистентности, нет подтверждений - только доставка активным подписчикам в реальном времени
  • **Два Redis-соединения обязательны** для адаптера: subscriber mode блокирует все команды кроме SUB/UNSUB на одном соединении
  • **PSUBSCRIBE с wildcard-паттернами** (`chat:*`, `notifications:user:*`) позволяет подписываться на группы каналов без их явного перечисления
  • **Для гарантированной доставки** - Redis Streams или BullMQ; Pub/Sub только для fire-and-forget синхронизации

Связанные темы

Redis Pub/Sub - часть более широкой экосистемы инструментов реального времени и масштабирования:

  • Redis Streams — Альтернатива с персистентностью и consumer groups для надёжной доставки
  • WebSocket горизонтальное масштабирование — Проблема, которую решает Redis Adapter - синхронизация stateful соединений
  • BullMQ — Очередь задач поверх Redis с гарантиями доставки и retry-логикой
  • Socket.io комнаты и пространства имён — Абстракции Socket.io, которые Redis Adapter распределяет по узлам кластера

Вопросы для размышления

  • Когда в проекте стоит выбрать Redis Pub/Sub вместо прямых HTTP-вызовов между сервисами для синхронизации?
  • Как изменится архитектура Socket.io приложения при переходе с одного сервера на кластер из 5 узлов?
  • Что произойдёт с чатом реального времени, если Redis недоступен на 30 секунд - и как это обработать gracefully?

Связанные уроки

  • db-19-redis
Redis Pub/Sub

0

1

Войти