Pub/Sub против Streams: что выбрать
В Redis есть два способа передавать события между сервисами — лёгкий и быстрый Pub/Sub без памяти и надёжный Streams с историей. Разберём, чем они отличаются и когда какой выбрать.
Pub/Sub — модель «опубликовал и забыл»: сообщение получают только те подписчики, кто подключён прямо сейчас; история не хранится. Streams — журнал событий, который хранит сообщения на диске, поддерживает группы потребителей, подтверждения и переигрывание с любой точки.
Зачем на практике
Передача событий — обыденная задача: уведомить другие сервисы о новом заказе, разослать обновление в реальном времени, поставить задачу в очередь на обработку. Redis предлагает два инструмента с противоположными приоритетами. Pub/Sub оптимизирован под минимальную задержку и простоту, но ничего не гарантирует. Streams оптимизирован под надёжность: ни одно событие не потеряется, его можно переобработать. Выбор между ними — это выбор между «быстро и эфемерно» и «надёжно и с историей».
Pub/Sub: fire-and-forget
Модель проста: издатель шлёт сообщение в канал командой PUBLISH, а все, кто подписан на канал через SUBSCRIBE в этот момент, его получают. Сообщение нигде не сохраняется — отдал подписчикам и забыл.
# подписчик (одно соединение, слушает канал)
127.0.0.1:6379> SUBSCRIBE news
Reading messages... (press Ctrl-C to quit)
# издатель (другое соединение)
127.0.0.1:6379> PUBLISH news "Вышла новая статья"
(integer) 1 # число подписчиков, получивших сообщение
Число в ответе PUBLISH — это сколько подписчиков получили сообщение прямо сейчас. Если подписчиков нет, сообщение просто исчезает: PUBLISH вернёт 0, и никто никогда его не увидит. Подписчик, который подключится через секунду, пропущенные сообщения уже не получит — истории нет.
Когда Pub/Sub — правильный выбор
Pub/Sub идеален там, где потеря отдельного сообщения не критична, а важна скорость и простота: живые уведомления в интерфейсе, обновление онлайн-дашборда, рассылка «инвалидируй кеш» по сервисам, чат-комнаты «здесь и сейчас». Если подписчик на мгновение отвалился и пропустил пару апдейтов — ничего страшного, следующий придёт.
Streams: надёжность и переигрывание
Stream — это упорядоченный журнал записей с уникальными ID (по времени). События добавляются командой XADD и остаются в потоке, пока вы их не удалите. Читать можно как угодно: с начала, с конкретного ID, только новые — и сколько угодно раз.
127.0.0.1:6379> XADD orders * sku A-100 qty 2
"1718900000000-0" # автоматически присвоенный ID события
127.0.0.1:6379> XADD orders * sku B-200 qty 1
"1718900000050-0"
# прочитать всю историю потока с самого начала
127.0.0.1:6379> XRANGE orders - +
1) 1) "1718900000000-0"
2) 1) "sku" 2) "A-100" 3) "qty" 4) "2"
2) 1) "1718900000050-0"
2) 1) "sku" 2) "B-200" 3) "qty" 4) "1"
В отличие от Pub/Sub, потребитель, который подключился позже, спокойно прочитает все накопленные события — они никуда не делись. Это и есть переигрывание (replay).
Группы потребителей и подтверждения
Главная сила Streams — consumer groups. Группа позволяет нескольким воркерам делить поток: каждое событие уходит ровно одному потребителю в группе, а после обработки воркер подтверждает его командой XACK. Неподтверждённые события остаются в «списке ожидания» (PEL) и при сбое воркера могут быть переданы другому — гарантия «хотя бы один раз».
# создать группу, читающую orders с самого начала
XGROUP CREATE orders workers 0
# воркер забирает новые сообщения для себя (имя consumer = w1)
XREADGROUP GROUP workers w1 COUNT 10 STREAMS orders >
# обработали — подтверждаем по ID
XACK orders workers 1718900000000-0
Смоделируем разницу в доставке: Stream хранит все события и отдаёт их позднему потребителю, а Pub/Sub доставляет только тем, кто был онлайн.
events = []
def stream_publish(msg): events.append(msg) # XADD: остаётся в потоке
def pubsub_publish(msg, online): return list(online) # доставлено лишь онлайн
stream_publish("e1"); stream_publish("e2"); stream_publish("e3")
print("Stream хранит историю:", events)
print("Поздний потребитель переиграет:", events) # доступны все три
online_at_e2 = ["A"] # подписчик B офлайн
delivered = pubsub_publish("e2", online_at_e2)
print("Pub/Sub доставил e2 только:", delivered, "(B потерял сообщение навсегда)")
Вывод:
Stream хранит историю: ['e1', 'e2', 'e3'] Поздний потребитель переиграет: ['e1', 'e2', 'e3'] Pub/Sub доставил e2 только: ['A'] (B потерял сообщение навсегда)
Что выбрать: сравнение
| Свойство | Pub/Sub | Streams |
| История сообщений | нет | да, хранится в потоке |
| Доставка офлайн-подписчику | сообщение теряется | прочитает позже |
| Переигрывание (replay) | невозможно | с любого ID, многократно |
| Подтверждения (ack) | нет | да (XACK, PEL) |
| Несколько воркеров делят нагрузку | нет (все получают всё) | да (consumer groups) |
| Память | почти не растёт | растёт, нужна обрезка (MAXLEN) |
| Когда брать | живые уведомления, инвалидация кеша | очереди задач, событийные конвейеры, надёжность |
Как это работает под капотом
В Pub/Sub сервер держит таблицу «канал → список подписанных соединений». При PUBLISH он проходит этот список и пишет сообщение в выходной буфер каждого подписчика — и тут же забывает о сообщении, нигде его не сохраняя. Нет подписчиков — некуда писать, сообщение исчезает. Stream же — полноценная структура данных в keyspace: записи лежат в памяти (и попадают в RDB/AOF при включённой персистентности), у каждой свой монотонный ID. Группа потребителей хранит «последний выданный ID» и PEL — список выданных, но ещё не подтверждённых записей по каждому потребителю. Именно PEL и XACK дают гарантию обработки: пока запись не подтверждена, она числится незавершённой и может быть перевыдана через XCLAIM. Поскольку история копится, поток обрезают по длине (XADD ... MAXLEN ...) или по времени, иначе память будет расти бесконечно.
Частые ошибки
- Очередь задач на Pub/Sub. Упал воркер — задачи, пришедшие в это время, потеряны навсегда. Для очередей берите Streams.
- Ждать доставку офлайн-подписчику в Pub/Sub. Кто не подключён в момент
PUBLISH, сообщение не получит — истории нет. - Streams без обрезки. Без
MAXLEN/обрезки по времени поток растёт неограниченно и съедает память. - Забыть XACK. Без подтверждения записи копятся в PEL и считаются необработанными; кажется, что «всё зависло».
- Брать Streams ради простого уведомления. Где потеря сообщения не страшна и важна задержка — Pub/Sub проще и легче.
Итоги
- Pub/Sub — «опубликовал и забыл»: быстро, просто, без истории; доходит только до тех, кто онлайн.
- Streams — журнал событий с историей, подтверждениями и группами потребителей: ничего не теряется.
- Только Streams умеют переигрывание и распределение нагрузки между воркерами (consumer groups +
XACK). - Pub/Sub почти не ест память; Streams растут — обрезайте поток через
MAXLENили по времени. - Выбор: живые уведомления и инвалидация кеша — Pub/Sub; очереди задач и надёжные событийные конвейеры — Streams.