Redis data structures и моделирование данных

В Redis проектирование начинается не с таблиц, а с операций. Нужно спросить: какие запросы будут частыми, какие должны быть атомарными, какие данные можно хранить с TTL, а какие нельзя потерять.

Плохая модель Redis обычно выглядит так:

text
key: user:42 value: огромный JSON со всем профилем, настройками, прогрессом и статистикой

Любое изменение требует прочитать весь JSON, распарсить, изменить поле, сериализовать обратно и записать. Хорошая модель использует структуры Redis так, чтобы команда меняла ровно нужную часть данных.


Ключи и naming convention

Redis не знает схемы, но схему должны знать вы. Обычно ключи строят через ::

text
user:42 user:42:settings course:go-basics:lessons lesson:redis-overview:views rate:user:42:login:2026-04-30T10:35

Правила:

  • включайте тип сущности и идентификатор;
  • не делайте ключи слишком длинными, но и не экономьте до нечитабельности;
  • для multi-tenant систем добавляйте tenant prefix;
  • держите TTL-ключи отдельно от долговременных;
  • не полагайтесь на возможность "найти всё по prefix" через KEYS.
  • версионируйте ключи или payload, если формат меняется: course:v2:42;
  • документируйте owner ключа: какой use case пишет, кто читает, кто инвалидирует.

Минимальный key contract для production обычно включает:

ПолеПример
key patternratedesk:rate:{pair}:latest
typeString или Hash
TTL5 минут плюс jitter
source of truthPostgreSQL rates
invalidationdelete/update после commit обновления курса
outage policymiss/read Redis error -> читать source of truth
max expected sizeдо 2 KB payload, не список истории

Такой контракт не заменяет проектное задание, но помогает ревьюеру понять, что ключ не появился случайно в середине handler'а.


Структуры данных

String

String - самый универсальный тип. Это byte array до 512 MB, но на практике такие размеры почти всегда ошибка.

КомандаСмыслСложность
GET keyполучить значениеO(1)
`SET key value EX seconds [NXXX]`записать значение
INCR keyувеличить integerO(1)
MGET key...получить несколько ключейO(N)
SETNX key valueзаписать только если нет ключаO(1)

Примеры:

text
SETEX lesson:redis-overview:html 600 "<h1>...</h1>" INCR lesson:redis-overview:views SET lock:billing:42 token-abc NX PX 10000

String подходит для кеша сериализованных объектов, счётчиков, feature flags, tokens. Если нужно часто менять отдельные поля объекта, лучше смотреть на Hash.


Hash

Hash хранит набор field-value внутри одного ключа.

text
HSET user:42 name "Pavel" role "student" timezone "Europe/Moscow" HGET user:42 name HINCRBY user:42 solved_tasks 1
КомандаСмыслСложность
HGET key fieldполучить полеO(1)
HSET key field valueзаписать полеO(1)
HMGET key f1 f2получить несколько полейO(N)
HGETALL keyполучить весь hashO(N)
HINCRBY key field nувеличить числовое полеO(1)

Hash удобен для объектов среднего размера:

text
user:42 id=42 name=Pavel avatar_url=https://... solved_tasks=17

Но не нужно складывать в один hash миллион полей. Большой hash становится big key: он плохо удаляется, реплицируется и может создавать задержки.


List

List - двусвязный список строк. Быстрые операции по краям:

text
LPUSH notifications:user:42 "new-comment" LTRIM notifications:user:42 0 99 LRANGE notifications:user:42 0 9
КомандаСмыслСложность
LPUSH/RPUSHдобавить слева/справаO(1)
LPOP/RPOPудалить слева/справаO(1)
LRANGE key start stopдиапазонO(S+N)
BLPOP key timeoutblocking popO(N keys)

List подходит для простых очередей и "последних N элементов". Но для событий с подтверждением обработки лучше использовать Streams: List не хранит consumer group state и pending entries.


Set

Set - неупорядоченное множество уникальных строк.

text
SADD course:redis:students user:42 user:100 SISMEMBER course:redis:students user:42 SCARD course:redis:students
КомандаСмыслСложность
SADDдобавить элементыO(N)
SREMудалить элементыO(N)
SISMEMBERпроверить наличиеO(1)
SMEMBERSполучить всеO(N)
SINTER/SUNION/SDIFFоперации множествзависит от размеров

Set хорошо моделирует связи "пользователь состоит в группе", "урок отмечен избранным", "роль назначена пользователю".

text
user:42:favorites = {lesson:redis-overview, lesson:docker-basics} lesson:redis-overview:liked_by = {user:42, user:77}

Если нужен порядок или score, используйте Sorted Set.


Sorted Set

Sorted Set хранит уникальные members и числовой score. Элементы упорядочены по score.

text
ZADD leaderboard:weekly 150 user:42 ZINCRBY leaderboard:weekly 10 user:42 ZREVRANGE leaderboard:weekly 0 9 WITHSCORES ZRANK leaderboard:weekly user:42
КомандаСмыслСложность
ZADDдобавить/обновить scoreO(log N)
ZINCRBYувеличить scoreO(log N)
ZRANGE/ZREVRANGEдиапазон по рангуO(log N + M)
ZRANGEBYSCOREдиапазон по scoreO(log N + M)
ZREMудалитьO(log N)

Сценарии:

  • leaderboard;
  • топ популярных уроков;
  • delayed jobs, где score - timestamp;
  • sliding window rate limit, где score - время запроса.
text
rate:user:42 score=1714471200123 member=req-1 score=1714471201440 member=req-2

Streams

Stream - append-only структура для событий. Каждый entry имеет ID и набор полей.

text
XADD submissions * user_id 42 lesson redis-overview status accepted XREAD COUNT 10 STREAMS submissions 0

С consumer groups:

text
XGROUP CREATE submissions workers $ MKSTREAM XREADGROUP GROUP workers worker-1 COUNT 10 BLOCK 5000 STREAMS submissions > XACK submissions workers 1714471200000-0

Streams дают:

  • хранение событий в Redis;
  • чтение несколькими consumer'ами;
  • pending entries для сообщений, которые выданы, но не подтверждены;
  • replay старых сообщений в пределах retention.

Это не Kafka, но для локальных backend-задач и небольших event pipelines Streams часто достаточно.


Bitmap

Bitmap - операции над битами внутри String. Удобно для компактных boolean-флагов.

text
SETBIT attendance:2026-04-30 42 1 GETBIT attendance:2026-04-30 42 BITCOUNT attendance:2026-04-30

Если user ID плотные и числовые, можно хранить посещаемость или активность очень компактно:

text
1_000_000 пользователей = примерно 125 KB на один день активности

Недостаток: sparse ID вроде UUID плохо подходят, потому что индекс должен быть integer offset.


HyperLogLog

HyperLogLog даёт приближённое количество уникальных элементов с маленькой памятью.

text
PFADD unique:lesson:redis-overview user:42 user:77 PFCOUNT unique:lesson:redis-overview PFMERGE unique:course:redis unique:lesson:1 unique:lesson:2

Сценарий: "сколько уникальных пользователей открыли урок". Если точность до одного пользователя критична, нужен Set или основная БД. Если нужна оценка с небольшой погрешностью - HyperLogLog экономит память.


Geospatial

Redis умеет хранить координаты и искать объекты рядом:

text
GEOADD mentors:geo 37.6173 55.7558 mentor:moscow GEOSEARCH mentors:geo FROMLONLAT 37.6 55.7 BYRADIUS 10 km WITHDIST

Под капотом используется Sorted Set с geohash-like score. Это удобно для простого nearby search, но не заменяет полноценную гео-БД, если нужны сложные полигоны, маршруты и геоаналитика.


Моделирование: объект или индексы

Допустим, есть курс и нужно быстро:

  • получить карточку курса;
  • узнать количество студентов;
  • показать топ студентов;
  • проверить, записан ли пользователь.

Модель может быть такой:

text
course:redis Hash title, level, description course:redis:students Set user ids course:redis:leaderboard ZSet user id -> points course:redis:stats Hash views, completed_count

Так Redis хранит не "таблицу course", а несколько структур под конкретные операции. Это нормально, но увеличивает ответственность приложения: нужно поддерживать согласованность между ключами.

Согласованность между ключами

Redis не поддерживает foreign keys и не знает, что course:redis:students связан с course:redis:leaderboard. Если запись меняет несколько структур, нужно выбрать стратегию:

  • одна атомарная команда, если структура позволяет;
  • MULTI/EXEC или Lua, если нужно обновить несколько ключей вместе;
  • eventual consistency с периодическим repair job, если небольшая рассинхронизация допустима;
  • source of truth в PostgreSQL и Redis как производный read model/cache.

Для кешей чаще всего выигрывает последний вариант: Redis-ключ можно удалить и восстановить из основной БД. Для счётчиков и рейтингов нужно заранее решить, что происходит при потере последних секунд данных, eviction или failover.

Cardinality и tenant boundaries

Проектируя ключи, оценивайте не только один пример, а количество ключей и элементов:

text
tenants * users * courses * days = потенциальный key count

Ошибки, которые часто всплывают поздно:

  • ключ на каждый request без TTL;
  • tenant prefix забыли, и данные разных клиентов смешались;
  • Set растёт годами без retention;
  • ZSet использует user id как label в метриках или как бесконечный high-cardinality namespace;
  • в Redis Cluster все ключи получили один hash tag вроде {global} и перегрели один slot.

Команды, которые требуют осторожности

КомандаПочему опасна
KEYS patternблокирует event loop при большом keyspace
SMEMBERS huge_setвозвращает всё множество целиком
HGETALL huge_hashвозвращает все поля большого hash
LRANGE key 0 -1может вытащить огромный список
DEL big_keyудаление большого ключа может занять заметное время
SORT, SUNION по большим множествамCPU и память на одну команду могут заблокировать соседей
XREAD без retention-планаstream может расти без ограничения

В production обычно используют SCAN, лимитированные диапазоны, UNLINK для асинхронного удаления и метрики big keys.

Отдельная ловушка - administrative команды в приложении. FLUSHALL, CONFIG, DEBUG, MONITOR и похожие команды не должны быть доступны runtime-пользователю сервиса через ACL.


Вопросы на собеседовании

  • Чем Redis Hash отличается от JSON в String?
  • Когда выбрать Set, а когда Sorted Set?
  • Почему SMEMBERS может быть проблемой?
  • Какая сложность у ZADD и почему leaderboard обычно делают на Sorted Set?
  • Для чего нужен HyperLogLog и чем он хуже Set?
  • Почему List не всегда хорошая очередь?
  • Что такое Stream consumer group?
  • Как бы вы смоделировали лайки, подписки и топ пользователей в Redis?
  • Что должно быть в key contract перед добавлением Redis-ключа в production?
  • Как понять, что выбранная модель создаст big key или hotspot?

Практика

  1. Спроектируйте Redis-ключи для прогресса пользователя по курсу: завершённые уроки, текущий lesson slug, количество решённых задач, дата последней активности.
  2. Выберите типы Redis для задач: уникальные просмотры урока, последние 20 уведомлений, рейтинг недели, поиск менторов рядом с городом.
  3. Для каждого ключа из практики укажите команды чтения и записи, а также команды, которых стоит избегать при росте данных.
  4. Оцените, какие ключи могут стать big keys, и предложите способ разбиения.
  5. Для одного ключа RateDesk опишите owner, TTL, source of truth, invalidation и max expected size.

Интерактивная практика

Quiz+10 XP

Какую структуру Redis чаще всего выбирают для leaderboard с score?

  • String
  • Hash
  • Sorted Set
  • HyperLogLog
Predict+15 XP

Что выведет этот код?

go
package main import "fmt" func readPattern(fullKeyspace bool) string { if fullKeyspace { return "danger" } return "bounded" } func main() { fmt.Println(readPattern(true)) fmt.Println(readPattern(false)) }
Задача+20 XP

Реализуй StructureFor: уникальные элементы — Set, рейтинг — SortedSet, durable очередь событий — Stream.