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?