Вопрос проверяет, умеете ли вы отделять “оперативные данные для быстрого ответа” от “сырых данных для аналитики”, выбирать подходящее хранилище и строить быстрые агрегации без тяжёлых запросов на критическом пути.
Для быстрого runtime-доступа обычно делают отдельный слой “профиля пользователя”: компактные данные, оптимизированные под чтение, часто в Redis или в отдельной таблице/витрине. Сырые события (клики, просмотры) хранят отдельно, а агрегаты (топ категорий, счётчики за 7/30 дней) считаются асинхронно и складываются в быстрый store. В запросе сервиса читают уже готовые агрегаты, а не пересчитывают их. Важно продумать TTL/инвалидацию, версионирование схемы профиля и частичные обновления, чтобы не перетирать данные.
Runtime-профиль пользователя должен быть маленьким, быстрым и предсказуемым по времени чтения. Сырые события и тяжёлая аналитика живут отдельно.
Обычно выделяют 2-3 слоя:
События (event log)
максимум детализации, много записей
используются для оффлайн-агрегаций и расследований
Агрегаты (aggregates)
счётчики, окна времени, топы
готовые значения для онлайна
Runtime-профиль (runtime profile)
компактная “карточка” пользователя для быстрых решений в запросе
хранит только то, что реально нужно на критическом пути
Практичный состав:
идентификаторы и статусы (например, user_id, segment)
настройки/флаги (feature flags)
права/роль
быстрые агрегаты:
счётчики событий за окна (7/30 дней)
top-N категорий
“последняя активность”
технические поля: version, updated_at
Определение: Aggregate — заранее посчитанное значение (например, “сколько покупок за 7 дней”), которое быстро читается и не требует тяжёлого пересчёта.
Есть два основных пути, часто их комбинируют:
Пакетный пересчёт (batch)
периодически (например, раз в 5-30 минут) пересчитываем агрегаты
хорошо для нестрогой актуальности
Потоковые обновления (stream-like)
по событию обновляем счётчики/окна
хорошо, когда нужна почти мгновенная реакция
Выбор структуры зависит от того, как читаете.
Частые варианты:
HASH для профиля: user:<id>:profile
ZSET для top-N интересов: user:<id>:top_categories (score = вес)
отдельные ключи-счётчики для окон (если нужно просто читать число)
Пример: профиль в HASH (идея):
// HSET user:123:profile segment "sports" updated_at "1700000000" version "3"
Если нужно top-N:
обновляем ZINCRBY по категории
читаем ZREVRANGE для лучших N
Типичные сложности:
два обновления приходят одновременно и перетирают друг друга
смена схемы профиля ломает читателей
Решения:
частичные обновления (обновлять только нужные поля)
версия профиля (version) и миграции на чтении
дедупликация событий (если события могут дублироваться)
атомарные операции Redis, где возможно
Если данные могут устаревать:
ставим TTL на профиль или на части профиля
используем stale-профиль при сбоях обновления (лучше “чуть устарело”, чем 500)
Важно:
TTL подбирается под бизнес (минуты/часы/дни)
критичные поля можно хранить без TTL или обновлять чаще
Онлайн-сервис должен делать:
1-2 обращения к Redis максимум
понятную деградацию, если профиля нет (cold start)
Быстрый runtime-доступ достигается разделением: события храним отдельно, агрегаты считаем асинхронно, а в Redis (или аналогичном быстром store) держим компактный runtime-профиль и top-N структуры. В запросе читаем готовое, а не пересчитываем.