Redis: быстрые данные рядом с приложением

Redis часто называют "кешем", но это слишком узкое описание. Redis - это in-memory data store: сервер, который хранит данные в оперативной памяти и даёт к ним доступ через сетевой протокол. Он умеет работать как кеш, очередь, хранилище с TTL, счётчик, rate limiter, pub/sub broker, backend для сессий и координатор для некоторых распределённых сценариев.

Главная идея Redis: держать горячие данные рядом с приложением и выполнять простые операции очень быстро.

text
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. Но логика выполнения команд над структурами данных остаётся последовательной.

text
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, массив или ошибку.

Команда:

text
SET user:42:name "Pavel"

В RESP выглядит примерно так:

text
*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
Streamappend-only log, consumer groups, обработка событий
Bitmapкомпактные boolean-флаги по индексам
HyperLogLogприближённый подсчёт уникальных значений
Geospatialкоординаты и поиск рядом

Из-за структур данных Redis часто позволяет не забирать объект в приложение, менять его и сохранять обратно. Например, счётчик можно увеличить атомарной командой:

text
INCR course:42:views

А участника добавить во множество:

text
SADD course:42:students user:1001

Это уменьшает race condition'ы и сетевые round-trip'ы.


Latency: где теряется время

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

text
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 и возвращает клиенту.

text
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 здесь естественно ложится на доменную модель:

text
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 идеально подходит для рейтингов:

text
ZADD leaderboard 1840 user:42 ZREVRANGE leaderboard 0 9 WITHSCORES

Redis как часть архитектуры

Redis нужно воспринимать как отдельную зависимость со своими отказами. Он может быть недоступен, медленен, перегружен, очищен eviction-политикой или отставать в репликации.

Хороший backend не должен превращать кеш в единственную точку истины без осознанного решения. Если Redis используется как кеш, приложение должно уметь пережить miss или временную недоступность. Если Redis используется как очередь или lock service, требования к durability и consistency нужно проговорить заранее.

text
Можно потерять данные? Нет -> Redis только с persistence/репликацией или другой брокер Данные можно пересчитать? Да -> Redis cache/counter подходит Нужна строгая транзакционность? Да -> основная БД, Redis как вспомогательный слой Нужна задержка < 5 ms? Redis может помочь, если сеть и команды спроектированы правильно

Политика деградации

Перед добавлением Redis нужно явно ответить на вопрос: что делает сервис, если Redis недоступен или возвращает медленные ответы?

СценарийОбычно безопасная политикаПочему
кеш публичного урокаfail-open: читать PostgreSQLRedis ускоряет, но не владеет истиной
кеш результата конверсииfail-open или stale-if-errorзависит от freshness SLA и допустимой старости курса
login rate limitfail-closed или stricter local fallbackлучше временно ограничить злоупотребление, чем открыть brute force
distributed lock для rebuild cachebest-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?

Практика

  1. Опишите, какие данные учебной платформы можно держать в Redis: профиль пользователя, прогресс уроков, список курсов, счётчики просмотров, сессии. Для каждого пункта решите: Redis - кеш или источник истины.
  2. Нарисуйте схему cache-aside для эндпоинта GET /api/lessons/{slug} и укажите, где возможен cache stampede.
  3. Подберите структуры данных Redis для задач: leaderboard студентов, одноразовый код входа, множество купленных курсов, последние 100 событий пользователя.
  4. Для двух Redis-сценариев RateDesk сформулируйте fail-open/fail-closed и stale data policy без описания конкретных issue проекта.

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

Quiz+10 XP

Для какого сценария Redis чаще всего подходит лучше всего?

  • Как единственный источник истины для финансовых операций
  • Как быстрый слой для горячих или временных данных рядом с приложением
  • Как замена PostgreSQL для сложных SQL-запросов
  • Как публичный storage без авторизации, потому что данные в памяти
Predict+15 XP

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

go
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)) }
Задача+20 XP

Реализуй StoreFor: durable business facts должны идти в postgres, временные данные можно хранить в redis, а неопределённый сценарий по умолчанию оставь в postgres.