Redis data structures и моделирование данных
В Redis проектирование начинается не с таблиц, а с операций. Нужно спросить: какие запросы будут частыми, какие должны быть атомарными, какие данные можно хранить с TTL, а какие нельзя потерять.
Плохая модель Redis обычно выглядит так:
key: user:42
value: огромный JSON со всем профилем, настройками, прогрессом и статистикой
Любое изменение требует прочитать весь JSON, распарсить, изменить поле, сериализовать обратно и записать. Хорошая модель использует структуры Redis так, чтобы команда меняла ровно нужную часть данных.
Ключи и naming convention
Redis не знает схемы, но схему должны знать вы. Обычно ключи строят через ::
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 pattern | ratedesk:rate:{pair}:latest |
| type | String или Hash |
| TTL | 5 минут плюс jitter |
| source of truth | PostgreSQL rates |
| invalidation | delete/update после commit обновления курса |
| outage policy | miss/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 [NX | XX]` | записать значение |
INCR key | увеличить integer | O(1) |
MGET key... | получить несколько ключей | O(N) |
SETNX key value | записать только если нет ключа | O(1) |
Примеры:
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 внутри одного ключа.
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 | получить весь hash | O(N) |
HINCRBY key field n | увеличить числовое поле | O(1) |
Hash удобен для объектов среднего размера:
user:42
id=42
name=Pavel
avatar_url=https://...
solved_tasks=17
Но не нужно складывать в один hash миллион полей. Большой hash становится big key: он плохо удаляется, реплицируется и может создавать задержки.
List
List - двусвязный список строк. Быстрые операции по краям:
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 timeout | blocking pop | O(N keys) |
List подходит для простых очередей и "последних N элементов". Но для событий с подтверждением обработки лучше использовать Streams: List не хранит consumer group state и pending entries.
Set
Set - неупорядоченное множество уникальных строк.
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 хорошо моделирует связи "пользователь состоит в группе", "урок отмечен избранным", "роль назначена пользователю".
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.
ZADD leaderboard:weekly 150 user:42
ZINCRBY leaderboard:weekly 10 user:42
ZREVRANGE leaderboard:weekly 0 9 WITHSCORES
ZRANK leaderboard:weekly user:42
| Команда | Смысл | Сложность |
|---|---|---|
ZADD | добавить/обновить score | O(log N) |
ZINCRBY | увеличить score | O(log N) |
ZRANGE/ZREVRANGE | диапазон по рангу | O(log N + M) |
ZRANGEBYSCORE | диапазон по score | O(log N + M) |
ZREM | удалить | O(log N) |
Сценарии:
- leaderboard;
- топ популярных уроков;
- delayed jobs, где score - timestamp;
- sliding window rate limit, где score - время запроса.
rate:user:42
score=1714471200123 member=req-1
score=1714471201440 member=req-2
Streams
Stream - append-only структура для событий. Каждый entry имеет ID и набор полей.
XADD submissions * user_id 42 lesson redis-overview status accepted
XREAD COUNT 10 STREAMS submissions 0
С consumer groups:
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-флагов.
SETBIT attendance:2026-04-30 42 1
GETBIT attendance:2026-04-30 42
BITCOUNT attendance:2026-04-30
Если user ID плотные и числовые, можно хранить посещаемость или активность очень компактно:
1_000_000 пользователей = примерно 125 KB на один день активности
Недостаток: sparse ID вроде UUID плохо подходят, потому что индекс должен быть integer offset.
HyperLogLog
HyperLogLog даёт приближённое количество уникальных элементов с маленькой памятью.
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 умеет хранить координаты и искать объекты рядом:
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, но не заменяет полноценную гео-БД, если нужны сложные полигоны, маршруты и геоаналитика.
Моделирование: объект или индексы
Допустим, есть курс и нужно быстро:
- получить карточку курса;
- узнать количество студентов;
- показать топ студентов;
- проверить, записан ли пользователь.
Модель может быть такой:
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
Проектируя ключи, оценивайте не только один пример, а количество ключей и элементов:
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?
Практика
- Спроектируйте Redis-ключи для прогресса пользователя по курсу: завершённые уроки, текущий lesson slug, количество решённых задач, дата последней активности.
- Выберите типы Redis для задач: уникальные просмотры урока, последние 20 уведомлений, рейтинг недели, поиск менторов рядом с городом.
- Для каждого ключа из практики укажите команды чтения и записи, а также команды, которых стоит избегать при росте данных.
- Оцените, какие ключи могут стать big keys, и предложите способ разбиения.
- Для одного ключа RateDesk опишите owner, TTL, source of truth, invalidation и max expected size.
Интерактивная практика
Какую структуру Redis чаще всего выбирают для leaderboard с score?
Что выведет этот код?
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))
}
Реализуй StructureFor: уникальные элементы — Set, рейтинг — SortedSet, durable очередь событий — Stream.