Redis Integration
Архитектурные решения по использованию Redis в GOLOOT.
1. Summary
Goal: Определить оптимальную стратегию использования Redis для текущей инфраструктуры (1 реплика backend).
User Value: Понимание почему in-memory решения используются вместо Redis для большинства задач, и когда это нужно пересмотреть.
2. Context
- Реплики: 1 (один контейнер backend)
- Железо: 8GB RAM, 4 cores
- Деплой: Dokploy, Docker Swarm
- Пользователи админки: 1 (владелец)
Горизонтальное масштабирование не планируется в ближайшее время.
3. Current Usage
Redis используется как cache-aside слой для горячих PG queries и для invite tracking.
Cache-Aside Layer (PG Query Cache)
Реализован в backend/src/common/cache/cache.service.ts. Все ключи — с префиксом goloot:v1:.
| Данные | Redis Key | TTL | Где кэшируется | Инвалидация |
|---|---|---|---|---|
| Активный сезон | goloot:v1:season:active | 5 мин | season.repository.ts | Admin lifecycle controller, season lifecycle job |
| Активные квесты | goloot:v1:quests:active:{category} | 2 мин | quest-progress.service.ts | Admin setup/quest controllers, lifecycle job |
| Детали кейса | goloot:v1:case:{caseId}:details | 10 мин | case-opening.service.ts | Admin case/reward controllers |
| Детали спина | goloot:v1:spin:{spinId}:details | 10 мин | user-spin.service.ts | Admin spin/reward controllers |
| Leaderboard | goloot:v1:leaderboard:{seasonId}:{limit}:{offset} | 2 мин | season.repository.ts | TTL-only (осознанный выбор) |
Other Usage
| Use Case | Ключи | Описание |
|---|---|---|
| Docs Invite Tracking | docs:invite:usage:* | Отслеживание использований invite-токенов с TTL |
| Fastify Rate Limit | plugin internal | Плагин может использовать Redis, но работает in-memory |
Потребление ресурсов
RAM: ~15-20 MB (базовый overhead + cache entries)
CPU: ~0-1% (GET/SET операции)
Disk: ~1-5 MB (RDB snapshots)
4. ADR: Rejected Improvements
Ниже — решения отложить внедрение Redis для различных задач.
- Rate Limiting
- SSE Events
- User Cache
- Analytics
- Sessions
- Promo Codes
- Other
- PG Query Cache
Secure Rate Limiting → Redis Counters
Файл: backend/src/common/middleware/secure-rate-limiting.middleware.ts
Текущая реализация: In-memory Map
Проблема
Rate limit counters не синхронизированы между инстансами при горизонтальном масштабировании.
Решение
Оставить in-memory Map.
Обоснование
- 1 реплика — все запросы идут в один процесс, in-memory работает идеально
- Проблема "обхода rate limit" возникает только при 2+ репликах, когда load balancer раскидывает запросы
- Redis добавит latency +1-5ms на каждый запрос без реальной пользы
Когда пересмотреть
- При переходе на
replicas: 2+ - При использовании PM2 cluster mode
- При развёртывании на нескольких серверах
SSE Real-time Events → Redis Pub/Sub
Файлы:
backend/src/domains/feed/controllers/feed.controller.ts— Live Feedbackend/src/domains/streaks/controllers/raffle-live.controller.ts— Raffle events
Текущая реализация: In-memory EventEmitter
Проблема
SSE клиенты на разных серверах не видят события друг друга.
Решение
Оставить EventEmitter.
Обоснование
- При 1 реплике все SSE клиенты подключены к одному процессу
- Проблема "клиенты на разных серверах" не существует
- Redis Pub/Sub добавит latency и усложнит reconnect логику
Когда пересмотреть
- При переходе на
replicas: 2+ - Если SSE критичен для бизнеса и нужна HA
UTM User Cache → Redis Cache
Файл: backend/src/domains/utm/services/caching/utm-user-cache.service.ts
Текущая реализация: In-memory Map с TTL 5 мин
Проблема
Кеш не синхронизирован между инстансами. Потеря при редеплое.
Решение
Оставить in-memory кеш.
Обоснование
- In-memory быстрее Redis (0ms vs 1-5ms latency)
- Проблема синхронизации не существует при 1 инстансе
- Кеш быстро прогревается (5 мин TTL), потеря при редеплое не критична
- Кеш на 10,000 записей занимает ~10-50MB — это приемлемо
Когда пересмотреть
- При переходе на
replicas: 2+ - Если кеш нужно шарить между сервисами
Analytics Caching
Файлы:
backend/src/domains/admin/services/banner-analytics.service.tsbackend/src/domains/admin/services/user-game-stats.service.ts
Проблема
Тяжёлые аналитические запросы могут тормозить админку.
Решение
Не добавлять кеширование.
Обоснование
- Админкой пользуется 1 человек — нет конкурентных запросов
- Загрузка 1-3 сек приемлема для одного пользователя
- Проверено на практике: аналитика на 2500 юзерах работает без задержек
- Лучше оптимизировать SQL запросы и добавить индексы
Когда пересмотреть
- Если загрузка > 5 сек
- Если появятся другие админы/менеджеры
- Если аналитика станет публичной
Session Storage
Проблема
При редеплое теряются сессии пользователей.
Решение
Не добавлять Redis session store.
Обоснование
- JWT токены stateless — сессия хранится в самом токене
- При редеплое JWT остаются валидными (подпись не меняется)
- Пользователи не разлогиниваются
- Redis session store нужен только для stateful sessions
Когда пересмотреть
- Если нужна функция "разлогинить все устройства"
- Если нужен аудит активных сессий
- При переходе на session-based auth
Race Condition Protection
Домен: backend/src/domains/promo-codes/
Проблема
Race condition при redemption промокода с лимитом использований.
Решение
Использовать database-level защиту вместо Redis.
Обоснование
- JavaScript однопоточный — race condition маловероятен при 1 реплике
- Для защиты достаточно:
- Database transaction с
SELECT FOR UPDATE - Unique constraint на
(userId, promoCodeId)
- Database transaction с
- Redis atomic counters нужны при тысячах redemptions/сек
Когда пересмотреть
- При переходе на
replicas: 2+ - При массовых промо-акциях с высоким трафиком
Steam API Response Cache
Обоснование отказа
- Steam API rate limits достаточно высокие
- Если кеш нужен — можно использовать in-memory
- Добавление Redis усложняет код без пользы
Leaderboard (Sorted Sets)
Обоснование отказа
- PostgreSQL с индексами справляется для текущих объёмов
ORDER BY score DESC LIMIT 100— быстро с правильными индексами- Redis Sorted Sets нужны при миллионах пользователей
PostgreSQL Query Caching → Redis
Статус: Реализовано (Февраль 2026)
Проблема
При росте до 10K DAU PostgreSQL обрабатывает ~2.3M queries/день. Многие запросы повторяются — одни и те же данные читаются тысячами пользователей:
| Данные | Queries/день (10K DAU) | Причина повторов |
|---|---|---|
| Активный сезон | ~200K | Каждая операция проверяет сезон |
| Список активных квестов | ~300K | Quest progress вызывается 30 раз/сессию |
| Детали кейса + rewards | ~50K | Каждое открытие загружает кейс с deep include |
| Leaderboard top-100 | ~30K | Популярная страница |
| Детали Spin + items | ~20K | Каждый спин загружает конфигурацию |
| Итого "горячих" | ~600K | ~25-30% всех queries |
Другие решения (rate limiting, SSE, sessions) — про синхронизацию между репликами. Этот use case — про снижение нагрузки на PostgreSQL, актуален даже при 1 реплике.
Решение
Реализован cache-aside слой (backend/src/common/cache/) для 5 типов горячих данных.
Реализация
Инфраструктура:
| Файл | Назначение |
|---|---|
common/cache/cache.service.ts | Cache-aside с graceful degradation |
common/cache/cache-keys.ts | Key schema, TTL constants, prefix patterns |
common/cache/index.ts | Singleton export |
config/redis.config.ts | Redis client initialization |
common/cache/cache.service.test.ts | Unit-тесты |
Что кешируется:
| Данные | Redis Key | TTL | Инвалидация | Ожидаемая экономия |
|---|---|---|---|---|
| Активный сезон | goloot:v1:season:active | 5 мин | Admin + lifecycle job | -99.8% queries |
| Активные квесты | goloot:v1:quests:active:{category} | 2 мин | Admin setup/quest + lifecycle job | -95% queries |
| Детали кейса | goloot:v1:case:{caseId}:details | 10 мин | Admin case/reward | -99.7% queries |
| Leaderboard | goloot:v1:leaderboard:{seasonId}:{limit}:{offset} | 2 мин | TTL-only | -97.6% queries |
| Детали Spin | goloot:v1:spin:{spinId}:details | 10 мин | Admin spin/reward | -99.3% queries |
Ключевые решения реализации:
- Envelope pattern
{ d: value }— отличает cache miss (nullот Redis) от закэшированногоnull(нет активного сезона) - Custom сериализация — BigInt (
{ __t: 'B', v: '...' }) и Date ({ __t: 'D', v: '...' }) для корректной работы с Prisma моделями - SCAN для инвалидации по префиксу — никогда не использовать
KEYS(блокирует Redis) - Fire-and-forget SET — запись в кэш не блокирует ответ клиенту
- Graceful degradation — при недоступности Redis запросы идут напрямую в PG
- Prometheus-метрики —
cache_hit_total,cache_miss_total,cache_error_totalпо key prefix
Инвалидация покрыта в 8 admin-контроллерах + lifecycle job. Подробнее: Caching Strategy.
TMA имеет особенность: пользователи открывают приложение, быстро совершают 3-5 действий и закрывают. Это создаёт burst-паттерн — короткие всплески из 10-20 запросов от одного юзера за 30-60 секунд. Кеш с TTL 1-5 мин эффективно покрывает такие burst'ы — данные загружаются из PG один раз в начале сессии, дальше все запросы идут из Redis.
Следующий этап масштабирования
- Per-user кэш (user profile, user quests) — при 5,000+ DAU
- Redis Sorted Sets для leaderboard — при 50,000+ пользователей в сезоне
- Rate limiting через Redis — при переходе на 2+ реплики
5. Architecture Diagram
Текущая архитектура
6. Decision Triggers
Когда пересмотреть решения:
| Событие | Что пересмотреть |
|---|---|
| 5,000+ DAU | Per-user кэш (profile, user quests) |
replicas: 2+ в Dokploy | Rate limiting, SSE pub/sub, User cache |
| Админка тормозит (> 5 сек) | Analytics caching |
| 50,000+ активных юзеров | Leaderboards (Redis Sorted Sets), общая архитектура |
| Нужна функция "разлогинить везде" | Session storage |
| Steam API rate limits | Steam response cache |
| Массовые промо-акции | Promo code race condition protection |
7. Decision History
| Дата | Решение | Обоснование |
|---|---|---|
| Январь 2026 | Отказ от Redis rate limiting | 1 реплика, in-memory достаточно |
| Январь 2026 | Отказ от Redis pub/sub для SSE | 1 реплика, EventEmitter достаточно |
| Январь 2026 | Отказ от Redis analytics cache | 1 пользователь админки, не тормозит |
| Январь 2026 | Отказ от Redis session store | JWT stateless, работает корректно |
| Январь 2026 | Оставить Redis для docs invite | 10-15 MB — несущественно, готовность к росту |
| Февраль 2026 | Добавлен план PG Query Cache | Capacity planning для 10K DAU выявил ~600K повторяющихся queries/день |
| Февраль 2026 | Реализован cache-aside layer | 5 типов данных: season, quests, cases, spins, leaderboard. Envelope pattern, graceful degradation, Prometheus metrics |
8. Related
- Caching Strategy — полная инвентаризация кешей и стратегия масштабирования
- Architecture Overview — общая архитектура системы
- Security Matrix — матрица защит по доменам