Redis: быстрые данные рядом с приложением
Redis часто называют "кешем", но это слишком узкое описание. Redis - это in-memory data store: сервер, который хранит данные в оперативной памяти и даёт к ним доступ через сетевой протокол. Он умеет работать как кеш, очередь, хранилище с TTL, счётчик, rate limiter, pub/sub broker, backend для сессий и координатор для некоторых распределённых сценариев.
Главная идея Redis: держать горячие данные рядом с приложением и выполнять простые операции очень быстро.
HTTP API
│
├── PostgreSQL: источник истины, транзакции, долговечность
│
└── Redis: горячие данные, TTL, счётчики, блокировки, очереди
Redis не заменяет PostgreSQL или другую основную базу данных в большинстве backend-систем. Он закрывает другой класс задач: низкая задержка, временное состояние, быстрая агрегация простых структур и снижение нагрузки на основное хранилище.
Что значит in-memory
Redis хранит рабочий набор данных в RAM. Поэтому доступ к данным обычно измеряется микросекундами на стороне сервера и миллисекундами с учётом сети, клиента и сериализации.
Цена такой скорости:
- память дороже диска;
- объём данных ограничен RAM и настройкой
maxmemory; - при неправильной persistence-настройке можно потерять последние изменения;
- big keys и hot keys быстро превращаются в production-проблемы.
Важно: "Redis быстрый" не означает "любой Redis-запрос бесплатный". Команда с асимптотикой O(N), ключ на сотни мегабайт или тысяча маленьких round-trip'ов подряд легко убьют latency.
Single-threaded event loop
Классический Redis исполняет команды в одном основном потоке. Это не значит, что Redis вообще не использует дополнительные потоки: современные версии могут использовать I/O threads для сетевого ввода-вывода и фоновые процессы/потоки для persistence. Но логика выполнения команд над структурами данных остаётся последовательной.
client A ─┐
client B ─┼── socket I/O ─► event loop ─► execute command ─► response
client C ─┘ │
└── данные Redis
Почему это работает:
- операции обычно короткие;
- нет тяжёлой синхронизации между worker'ами;
- структуры данных живут в памяти;
- Redis использует неблокирующий I/O и быстро переключается между клиентами.
Что из этого следует для разработчика:
- одна медленная команда задерживает остальные;
KEYS *, огромныйLRANGE, большой Lua-скрипт или удаление гигантского ключа могут создать latency spike;- Redis нужно проектировать так, чтобы каждая команда была маленькой и предсказуемой.
Redis удобен не потому, что "однопоточный быстрее многопоточного", а потому что выбранная модель убирает много накладных расходов и делает поведение команд более понятным.
RESP: как клиенты говорят с Redis
Redis использует RESP - Redis Serialization Protocol. Это простой текстово-бинарный протокол: клиент отправляет массив аргументов, сервер возвращает строку, число, bulk string, массив или ошибку.
Команда:
SET user:42:name "Pavel"
В RESP выглядит примерно так:
*3
$3
SET
$12
user:42:name
$5
Pavel
Обычно вы не пишете RESP руками. Go-клиент сам сериализует команды и парсит ответы. Но понимание протокола полезно: Redis-команда - это не HTTP-запрос с JSON, а компактный набор аргументов. Поэтому Redis хорошо чувствует себя в сценариях с большим количеством маленьких операций, особенно если использовать pipelining.
Структуры данных
Redis - не key-value store только со строками. Ключ указывает на значение определённого типа:
| Тип | Для чего используется |
|---|---|
| String | кеш JSON, счётчики, флаги, токены, простые значения |
| Hash | объект с полями: профиль, настройки, агрегат |
| List | очередь, лог последних событий, стек |
| Set | уникальные элементы: роли, подписки, лайки |
| Sorted Set | рейтинг, leaderboard, расписание по score |
| Stream | append-only log, consumer groups, обработка событий |
| Bitmap | компактные boolean-флаги по индексам |
| HyperLogLog | приближённый подсчёт уникальных значений |
| Geospatial | координаты и поиск рядом |
Из-за структур данных Redis часто позволяет не забирать объект в приложение, менять его и сохранять обратно. Например, счётчик можно увеличить атомарной командой:
INCR course:42:views
А участника добавить во множество:
SADD course:42:students user:1001
Это уменьшает race condition'ы и сетевые round-trip'ы.
Latency: где теряется время
Redis может выполнить команду очень быстро, но конечная задержка в приложении складывается из нескольких частей:
Go handler
│
├─ сериализация команды клиентом
├─ ожидание свободного connection из pool
├─ сеть до Redis
├─ очередь команд внутри Redis
├─ выполнение команды
├─ сеть обратно
└─ парсинг ответа и десериализация payload
Типичные причины плохой задержки:
- Redis далеко от приложения по сети;
- слишком маленький connection pool;
- команда работает с большим количеством элементов;
- ключ содержит огромный JSON;
- нет timeout'ов в
context.Context; - приложение делает 20 последовательных команд вместо pipeline или Lua-скрипта;
- включённая persistence даёт периодические паузы из-за диска или fork.
На практике Redis лучше держать в той же зоне/кластере, что и backend, измерять p95/p99 latency, следить за slowlog и не использовать Redis как свалку больших документов.
Распространённые backend-сценарии
Cache-aside
Приложение сначала смотрит в Redis. Если данных нет - читает PostgreSQL, кладёт результат в Redis с TTL и возвращает клиенту.
GET /courses/42
│
├─ Redis GET course:42
│ ├─ hit -> вернуть данные
│ └─ miss -> SELECT из PostgreSQL -> SETEX в Redis -> вернуть данные
Это снижает нагрузку на базу, но требует аккуратно работать с TTL, инвалидацией и cache stampede.
Sessions и auth state
Redis часто используют для серверных сессий, refresh token blacklist, одноразовых кодов и временных auth-состояний. TTL здесь естественно ложится на доменную модель:
SETEX otp:login:user:42 300 "123456"
Counters и rate limits
INCR, EXPIRE, sorted sets и Lua-скрипты позволяют делать счётчики просмотров, лимиты запросов, anti-spam и token bucket.
Очереди и события
Для простых задач можно использовать Lists, для более надёжной обработки - Streams с consumer groups. Но Redis не равен Kafka: retention, replay, масштабирование и delivery semantics у них разные.
Leaderboards и ранжирование
Sorted Set идеально подходит для рейтингов:
ZADD leaderboard 1840 user:42
ZREVRANGE leaderboard 0 9 WITHSCORES
Redis как часть архитектуры
Redis нужно воспринимать как отдельную зависимость со своими отказами. Он может быть недоступен, медленен, перегружен, очищен eviction-политикой или отставать в репликации.
Хороший backend не должен превращать кеш в единственную точку истины без осознанного решения. Если Redis используется как кеш, приложение должно уметь пережить miss или временную недоступность. Если Redis используется как очередь или lock service, требования к durability и consistency нужно проговорить заранее.
Можно потерять данные? Нет -> Redis только с persistence/репликацией или другой брокер
Данные можно пересчитать? Да -> Redis cache/counter подходит
Нужна строгая транзакционность? Да -> основная БД, Redis как вспомогательный слой
Нужна задержка < 5 ms? Redis может помочь, если сеть и команды спроектированы правильно
Политика деградации
Перед добавлением Redis нужно явно ответить на вопрос: что делает сервис, если Redis недоступен или возвращает медленные ответы?
| Сценарий | Обычно безопасная политика | Почему |
|---|---|---|
| кеш публичного урока | fail-open: читать PostgreSQL | Redis ускоряет, но не владеет истиной |
| кеш результата конверсии | fail-open или stale-if-error | зависит от freshness SLA и допустимой старости курса |
| login rate limit | fail-closed или stricter local fallback | лучше временно ограничить злоупотребление, чем открыть brute force |
| distributed lock для rebuild cache | best-effort fallback | ошибка lock не должна ломать бизнес-инвариант |
| Stream с важным событием | fail/retry, не терять молча | событие уже стало частью workflow |
Эта политика должна быть видна в коде, метриках и документации. Иначе Redis outage превращается в спор во время инцидента.
Stale data policy
Для кеша недостаточно выбрать TTL. Нужно решить:
- какая максимальная старость данных допустима для пользователя;
- кто удаляет или обновляет ключ после записи в source of truth;
- можно ли отдавать stale value, если PostgreSQL или Redis временно недоступны;
- как клиент поймёт, что ответ был рассчитан по устаревшим данным, если это важно для домена.
Например, карточка урока может жить в кеше 10 минут и инвалидироваться при публикации новой версии. Курс валют для RateDesk может иметь отдельный freshness threshold: если курс старше допустимого окна, лучше вернуть доменную ошибку freshness, а не тихо посчитать неверную сумму.
Минимальная наблюдаемость Redis
Даже для кеша нужны метрики:
cache_hit,cache_miss,cache_error,cache_stale_served;- latency Redis-команд по operation name, без raw key как label;
evicted_keys, memory usage, connected clients, slowlog;- pool stats клиента: ожидание соединения, timeouts, retries.
Не добавляйте user id, полный key, request id или текст ошибки как Prometheus labels: cardinality быстро убьёт мониторинг.
Вопросы на собеседовании
- Почему Redis часто быстрее дисковой базы данных?
- Что означает single-threaded event loop в Redis?
- Почему одна тяжёлая команда может повлиять на всех клиентов?
- Что такое RESP и зачем понимать, что Redis-команда - это набор аргументов?
- Чем Redis как кеш отличается от Redis как источника истины?
- Какие структуры данных Redis вы использовали бы для счётчика, рейтинга, очереди и сессии?
- Почему
KEYS *опасна в production? - Откуда берётся latency между Go-приложением и Redis?
- Что такое fail-open/fail-closed policy для Redis-зависимости?
- Чем TTL отличается от freshness policy?
Практика
- Опишите, какие данные учебной платформы можно держать в Redis: профиль пользователя, прогресс уроков, список курсов, счётчики просмотров, сессии. Для каждого пункта решите: Redis - кеш или источник истины.
- Нарисуйте схему cache-aside для эндпоинта
GET /api/lessons/{slug}и укажите, где возможен cache stampede. - Подберите структуры данных Redis для задач: leaderboard студентов, одноразовый код входа, множество купленных курсов, последние 100 событий пользователя.
- Для двух Redis-сценариев RateDesk сформулируйте fail-open/fail-closed и stale data policy без описания конкретных issue проекта.
Интерактивная практика
Для какого сценария Redis чаще всего подходит лучше всего?
Что выведет этот код?
package main
import "fmt"
func dependencyPolicy(required bool) string {
if required {
return "fail-closed"
}
return "fail-open"
}
func main() {
fmt.Println(dependencyPolicy(false))
fmt.Println(dependencyPolicy(true))
}
Реализуй StoreFor: durable business facts должны идти в postgres, временные данные можно хранить в redis, а неопределённый сценарий по умолчанию оставь в postgres.