Real-Time Backend
Redis Streams
Pub/Sub в Redis работает как радио: пока никто не слушает - сигнал теряется. Redis Streams - это уже магнитофон с перемоткой: каждое сообщение записано, его можно перечитать, и если обработчик упал - данные не потеряются.
- **Платежный сервис:** заказ добавляется в стрим `orders`, три воркера-обработчика читают через Consumer Group - каждый платеж обрабатывается ровно один раз, даже если один воркер перезапустился.
- **Аудит-лог действий пользователей:** все события пишутся в стрим, аналитический сервис читает их независимо от основного обработчика - оба получают полную картину без дублирования логики.
- **IoT-телеметрия:** тысячи датчиков шлют показания, MAXLEN обрезает стрим до последних 24 часов, несколько потребителей параллельно агрегируют данные по разным метрикам.
Redis Streams
Redis Streams - это персистентный лог сообщений, встроенный прямо в Redis. Каждое сообщение получает уникальный ID вида `1699000000000-0` (timestamp-sequence), и лог никуда не исчезает после прочтения - в отличие от обычного Pub/Sub.
Команда `XADD` добавляет запись: поле `*` означает автогенерацию ID. `XREAD` читает с позиции - можно читать с начала, с конкретного ID или только новые (`>`). Это позволяет нескольким сервисам читать один и тот же поток независимо.
Redis Streams vs Kafka: Streams живут в памяти (опционально персистируются через RDB/AOF), не требуют отдельного кластера и управляются одной командой. Kafka рассчитана на терабайты логов и тысячи партиций. Streams выигрывают при объемах до ~50 млн сообщений в день и когда Redis уже есть в стеке.
- `XADD key [MAXLEN ~ N] * field value ...` - добавить запись
- `XREAD COUNT N BLOCK ms STREAMS key id` - читать записи начиная с id
- `XRANGE key start end` - диапазон записей (- это начало, + конец)
- `XLEN key` - количество записей в стриме
- `XDEL key id` - удалить конкретную запись
Что происходит с сообщением в Redis Streams после того, как его прочитал один из подписчиков через XREAD?
Consumer Groups
Consumer Group - это именованная группа потребителей, которые делят один стрим между собой. Каждое сообщение достается ровно одному потребителю из группы - это позволяет горизонтально масштабировать обработку. Несколько групп могут читать один стрим независимо, каждая получает все сообщения.
Типичный паттерн - несколько воркеров в одной группе. Например, три инстанса payment-service запускают `XREADGROUP GROUP payment-processors consumer-{hostname}`. Redis автоматически распределяет сообщения между ними - нет нужды в Zookeeper или координаторе.
Разница от Kafka партиций: в Kafka нельзя иметь больше потребителей, чем партиций. В Redis Streams потребителей может быть сколько угодно - Redis сам балансирует. Но у Streams нет гарантии порядка между разными потребителями одной группы (в отличие от Kafka, где порядок гарантирован внутри партиции).
Три инстанса сервиса читают стрим `orders` через XREADGROUP из одной группы `processors`. Пришло 9 сообщений. Сколько сообщений получит каждый инстанс?
Pending Entries
Когда потребитель получает сообщение через XREADGROUP, Redis не считает его обработанным. Сообщение попадает в PEL - Pending Entry List. Там оно остается до явного `XACK`. Если воркер упал, не успев подтвердить, сообщение не потеряется - оно будет числиться как pending за этим потребителем.
Поле `delivery-count` в XPENDING показывает, сколько раз сообщение было доставлено. Если count > 3 - это poison message (обработчик падает на нем постоянно). Такие сообщения нужно перемещать в dead-letter stream и алертить, а не перепытываться бесконечно.
Воркер получил 10 сообщений через XREADGROUP, успешно обработал 7, потом упал. Что произойдет с оставшимися 3 сообщениями?
Stream Claim
XCLAIM и XAUTOCLAIM - команды для «перехвата» зависших сообщений. XCLAIM переназначает конкретный ID другому потребителю. XAUTOCLAIM (Redis 6.2+) сканирует весь PEL и забирает все сообщения старше указанного порога за один вызов - удобнее для автоматизации.
Полный production-цикл выглядит так: основные воркеры читают через `XREADGROUP ... >`, обрабатывают, делают `XACK`. Отдельный «janitor» процесс по расписанию вызывает `XAUTOCLAIM` с таймаутом в несколько минут, перехватывает зависшие сообщения и либо переобрабатывает их, либо отправляет в dead-letter стрим если `delivery-count` превысил лимит.
MAXLEN trimming - обязательная практика для production. Без ограничения стрим растет бесконечно и съедает память Redis. `XADD key MAXLEN ~ 100000 *` - тильда означает приближенную обрезку (быстрее, чем точная). Альтернатива - `XTRIM key MAXLEN ~ 100000` по расписанию, или хранить данные только за последние N часов через `XRANGE` + `XDEL`.
XACK автоматически удаляет сообщение из стрима
XACK только убирает сообщение из PEL (Pending Entry List) - подтверждает что потребитель обработал его. Само сообщение остается в стриме до XDEL или срабатывания MAXLEN.
PEL и стрим - две разные структуры. Стрим - это лог, PEL - список «взятых в работу» сообщений. XACK говорит «я закончил», но не «удали запись». Это позволяет другим группам или инструментам читать те же записи независимо.
Воркер `consumer-3` завис на одном сообщении уже 10 минут. XAUTOCLAIM вызван с min-idle-time 5 минут. Что произойдет с этим сообщением?
Итоги
- **Redis Streams = персистентный лог:** XADD добавляет, XREAD читает - сообщения не исчезают после прочтения, в отличие от Pub/Sub.
- **Consumer Groups = горизонтальное масштабирование:** каждое сообщение достается ровно одному потребителю группы; несколько групп получают все сообщения независимо.
- **PEL + XACK = at-least-once гарантия:** сообщение числится pending до явного XACK; XAUTOCLAIM возвращает зависшие сообщения в работу при падении воркера.
- **MAXLEN - обязателен в production:** без ограничения стрим растет бесконечно; `XADD key MAXLEN ~ N` держит только последние N записей без заметных потерь производительности.
Связанные темы
Redis Streams дополняют другие паттерны работы с очередями и логами:
- Pub/Sub — Альтернатива без персистентности - сообщения теряются если нет подписчика; Streams решают эту проблему
- BullMQ / Bull — Популярная Node.js очередь поверх Redis Lists; Streams дают больше контроля над группами и delivery-count
- Kafka — Тот же паттерн лога + consumer groups, но для масштаба в терабайты; Streams - легкая альтернатива если Redis уже в стеке
- Dead-letter Queue — Паттерн обработки poison messages из PEL - сообщения с delivery-count > N перемещаются в отдельный стрим для ручного разбора
Вопросы для размышления
- Сервис читает стрим через XREADGROUP, но периодически падает посередине батча. Какой механизм гарантирует, что ни одно сообщение не будет потеряно, и что нужно реализовать для автоматического восстановления?
- Два разных сервиса должны обрабатывать одни и те же события из стрима `events` - один строит аналитику, другой отправляет уведомления. Как организовать Consumer Groups чтобы каждый сервис получал все события независимо?
- Стрим растет и через месяц занимает 8 GB памяти. Какие стратегии ограничения размера существуют, и как выбрать между MAXLEN по количеству записей и удалением по времени?