Redis: как работает быстрое хранилище данных в памяти
PostgreSQL хорошо хранит связанные данные, поддерживает транзакции и выполняет сложные запросы. Но не каждую краткоживущую операцию выгодно каждый раз выполнять через основную базу.
Примеры:
- получить часто запрашиваемый профиль;
- сохранить сессию на 30 минут;
- ограничить число запросов;
- увеличить счётчик;
- выдать одноразовый код;
- хранить состояние задачи;
- передать событие worker-процессу;
- составить рейтинг.
Для таких задач часто используют Redis.
Redis - сервер структур данных, который в основном работает с данными в оперативной памяти и предоставляет команды для разных типов значений.
Ключ
-
структура данных
-
атомарная команда
Redis часто называют key-value database, но значением может быть не только строка. Redis поддерживает hashes, lists, sets, sorted sets, streams, JSON, географические индексы, вероятностные структуры и векторные наборы.
Redis может использоваться как:
- кэш;
- хранилище сессий;
- счётчик;
- rate limiter;
- очередь или журнал событий;
- хранилище быстрых временных состояний.
Обычно он дополняет PostgreSQL, а не заменяет его.
Клиент-серверная модель
Redis является отдельным сервером.
К нему подключаются:
- FastAPI;
- worker;
- n8n;
- CLI;
- другие сервисы.
Стандартный адрес:
redis://localhost:6379/0
localhost - адрес сервера
6379 - стандартный порт
0 - номер логической базы
Логические базы Redis разделяют ключи по номеру, но не являются полноценными независимыми базами с отдельными схемами и правами. В серьёзной архитектуре чаще используют отдельные экземпляры для разных задач.
Почему Redis быстрый
Фраза «Redis быстрый, потому что работает в памяти» верна только частично.
Скорость обеспечивают:
- хранение рабочего набора в RAM;
- оптимизированные структуры данных;
- простой сетевой протокол;
- атомарные серверные команды;
- отсутствие тяжёлого SQL-планирования для обычных операций.
Но память ограничена и дороже диска. Поэтому Redis требует контроля:
- размера данных;
- TTL;
- политики вытеснения;
- persistence;
- числа ключей;
- размера отдельных значений.
Ключи
Ключ идентифицирует значение.
session:7f23a
user:42
rate_limit:ip:192.0.2.1
order:105:status
Удобная схема именования:
область:объект:идентификатор:поле
Команды:
SET user:42:name "Jaguar"
GET user:42:name
DEL user:42:name
EXISTS user:42:name
TYPE user:42:name
Двоеточие не имеет специального значения для Redis. Это соглашение для читаемости.
Ключи тоже занимают память, поэтому слишком длинные имена при миллионах записей создают заметные расходы.
Основные структуры данных
Strings
String - последовательность байтов.
Она может хранить:
- текст;
- число;
- JSON-строку;
- бинарные данные;
- сериализованный объект.
SET page:views 100
INCR page:views
INCR атомарно увеличивает значение. Два клиента не потеряют обновление из-за одновременного чтения и записи.
Другие команды:
INCRBY
DECR
APPEND
MGET
MSET
Hashes
Hash хранит поля одного объекта.
HSET user:42 name "Jaguar" age 25 city "Moscow"
HGET user:42 name
HGETALL user:42
Hash похож на плоский словарь:
{
"name": "Jaguar",
"age": "25",
"city": "Moscow"
}
Он удобен для небольших объектов, если не нужны SQL-связи и сложные запросы.
Lists
List - упорядоченная последовательность строк.
LPUSH tasks task_1
LPUSH tasks task_2
RPOP tasks
Можно использовать как:
- очередь;
- стек;
- список последних событий.
Для надёжной обработки сообщений, подтверждений и нескольких consumer чаще лучше использовать Streams или специализированный брокер.
Sets
Set хранит уникальные строки без порядка.
SADD article:42:tags python api fastapi
SISMEMBER article:42:tags python
SMEMBERS article:42:tags
Поддерживает операции:
SINTER
SUNION
SDIFF
Примеры:
- уникальные посетители;
- роли пользователя;
- теги;
- пересечение интересов.
Sorted sets
Sorted set хранит уникальные элементы и числовой score.
ZADD leaderboard 1500 user_42
ZADD leaderboard 1700 user_17
ZREVRANGE leaderboard 0 9 WITHSCORES
Подходит для:
- рейтингов;
- приоритетных очередей;
- временных шкал;
- диапазонов по числовому значению.
Streams
Stream - добавляемый в конец журнал записей.
XADD orders * order_id 105 status created
Redis создаёт уникальный ID.
Чтение:
XRANGE orders - +
Streams поддерживают consumer groups:
один поток
-
несколько consumer
-
распределение сообщений
-
pending entries
-
ACK после обработки
Они подходят для:
- событий;
- телеметрии;
- очередей между сервисами;
- уведомлений;
- журналов действий.
Pub/Sub
Издатель отправляет сообщение в канал:
PUBLISH notifications "order created"
Подписчик слушает:
SUBSCRIBE notifications
Pub/Sub использует семантику at-most-once:
подписчик подключён
-
сообщение получено
подписчик отключён
-
сообщение потеряно
Сообщения не сохраняются как очередь.
Если требуется история, подтверждение и повторное чтение, лучше использовать Streams.
JSON, временные ряды и векторы
Современный Redis поддерживает дополнительные структуры:
- JSON;
- time series;
- probabilistic data types;
- geospatial;
- vector sets и векторный поиск.
Они полезны для:
- RAG;
- семантического поиска;
- телеметрии;
- approximate counting;
- географических запросов.
Но Redis стоит добавлять не из-за модного названия структуры, а из-за требований к скорости, обновлению и эксплуатации.
TTL
TTL - время жизни ключа.
SET reset_code:user_42 "834921" EX 300
Ключ существует 300 секунд.
Проверка:
TTL reset_code:user_42
Добавить срок к существующему ключу:
EXPIRE session:abc 1800
Применения:
- сессии;
- одноразовые коды;
- кэш;
- временные блокировки;
- rate limiting;
- промежуточные результаты.
После исчезновения ключа приложение должно корректно обработать cache miss или окончание срока.
Кэширование
Кэш хранит копию данных, чтобы не выполнять дорогую операцию повторно.
Шаблон cache-aside:
1. Приложение запрашивает Redis.
2. Если ключ найден, возвращает значение.
3. Если ключа нет, читает PostgreSQL.
4. Сохраняет результат в Redis с TTL.
5. Возвращает клиенту.
Псевдокод:
cached = redis.get(key)
if cached is not None:
return deserialize(cached)
value = database.load(user_id)
redis.set(key, serialize(value), ex=300)
return value
Cache hit
Данные найдены.
Cache miss
Ключ отсутствует, и приложение обращается к источнику истины.
Инвалидация
После изменения исходных данных старый кэш нужно:
- удалить;
- обновить;
- дождаться TTL.
Сложность кэша находится не в записи, а в поддержании допустимой актуальности.
Проблемы кэширования
Stale data
Redis хранит старую копию после изменения PostgreSQL.
Cache stampede
Популярный ключ истёк, и множество запросов одновременно пошло в базу.
Защита:
- случайный разброс TTL;
- lock на пересчёт;
- предварительное обновление;
- stale-while-revalidate.
Cache penetration
Запрашиваются несуществующие объекты, и каждый miss попадает в базу.
Иногда отрицательный результат кэшируют на короткое время.
Cache avalanche
Множество ключей истекает одновременно.
Eviction policy
При достижении лимита памяти Redis должен решить, что делать.
Политики включают:
noeviction - не удалять ключи, отклонять новые записи
allkeys-lru - удалять давно неиспользуемые ключи
allkeys-lfu - удалять редко используемые ключи
allkeys-random - удалять случайные ключи
volatile-* - выбирать только ключи с TTL
TTL и eviction различаются:
TTL - удаление по времени
Eviction - удаление из-за нехватки памяти
Для кэша eviction ожидаем. Для критичных данных автоматическое удаление может быть недопустимо.
Атомарность команд
Одна команда Redis выполняется атомарно относительно других команд.
INCR counter
безопаснее последовательности:
GET counter
↓
увеличить в приложении
↓
SET counter
Вторая схема создаёт гонку.
Полезные атомарные команды:
SET key value NX EX 30
HINCRBY
ZINCRBY
При проектировании лучше искать готовую серверную операцию вместо чтения и обратной записи.
MULTI, EXEC и WATCH
MULTI
INCR account:1
DECR account:2
EXEC
После MULTI команды ставятся в очередь. EXEC выполняет их последовательно.
Но это не SQL-транзакция:
- нет обычного rollback после ошибки команды;
- нет уровней изоляции PostgreSQL;
- для optimistic locking применяется
WATCH.
WATCH balance
GET balance
MULTI
SET balance 100
EXEC
Если отслеживаемый ключ изменился, клиент должен повторить операцию.
Lua-скрипты
Если несколько команд должны выполняться как единая операция, можно использовать Lua.
EVAL "
local current = redis.call('GET', KEYS[1])
if not current then
return 0
end
redis.call('DEL', KEYS[1])
return 1
" 1 lock:task
Скрипт выполняется атомарно. Но долгий скрипт блокирует обработку других команд, поэтому он должен быть коротким.
Временная блокировка
SET lock:report worker_17 NX PX 30000
NX - установить только если ключа нет
PX 30000 - TTL 30 секунд
Значение должно быть уникальным токеном владельца.
Освобождать lock нужно только при совпадении токена. Обычный DEL может удалить блокировку, уже полученную другим worker после истечения старого TTL.
Distributed lock требует анализа сбоев и не должен восприниматься как простая замена транзакции PostgreSQL.
Rate limiting
Простой fixed window может использовать:
ключ - rate:user:42:2026-08-01T12:45
INCR - увеличить счётчик
EXPIRE - удалить после окна
Для согласованности INCR и EXPIRE лучше объединить атомарным скриптом.
Другие алгоритмы:
- sliding window;
- token bucket;
- leaky bucket.
Persistence
Redis может сохранять данные на диск.
RDB
Создаёт снимки набора данных.
Плюсы:
- компактность;
- быстрое восстановление.
Риск:
- изменения после последнего снимка могут потеряться.
AOF
Записывает операции изменения в append-only log.
Плюс:
- меньший возможный интервал потери при подходящей fsync policy.
Минусы:
- дополнительные записи;
- более тяжёлый журнал.
RDB + AOF
Оба механизма можно сочетать.
No persistence
Подходит для полностью восстанавливаемого кэша.
Выбор зависит от роли Redis:
кэш - данные можно восстановить
критичное состояние - нужны persistence, backup и анализ гарантий
Репликация и высокая доступность
Redis поддерживает primary-replica репликацию.
primary - принимает записи
replica - асинхронно получает изменения
Асинхронность означает, что при сбое часть последних записей может не успеть попасть на replica.
Sentinel помогает обнаруживать сбой и выбирать новый primary.
Redis Cluster распределяет ключи между узлами и масштабирует объём и нагрузку.
Эти режимы требуют отдельного проектирования failover, клиентов и persistence.
Redis и FastAPI
Асинхронный клиент можно создать один раз при запуске приложения.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from redis.asyncio import Redis
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.redis = Redis.from_url(
"redis://localhost:6379/0",
decode_responses=True,
)
yield
await app.state.redis.aclose()
app = FastAPI(lifespan=lifespan)
Endpoint:
@app.get("/counter")
async def counter():
value = await app.state.redis.incr(
"requests:counter"
)
return {"value": value}
Клиент переиспользует пул соединений. Создавать новый клиент на каждый запрос не нужно.
Сессии
Redis подходит для серверных сессий.
cookie - непрозрачный session_id
Redis key - session:<id>
value - user_id, роли, metadata
TTL - срок жизни
Нужно учитывать защищённый cookie, logout, продление TTL и чувствительность данных.
Мониторинг
Полезно отслеживать:
- использованную память;
- число ключей;
- hit rate;
- evicted keys;
- expired keys;
- latency;
- число клиентов;
- replication lag;
- ошибки persistence;
- размер Streams и pending entries.
Команды:
INFO
MEMORY USAGE key
SLOWLOG GET
CLIENT LIST
KEYS * нельзя бездумно запускать на большом production-наборе. Для постепенного обхода используют SCAN.
Redis и PostgreSQL
| Вопрос | PostgreSQL | Redis |
|---|---|---|
| Основная модель | Таблицы и связи | Ключи и структуры |
| Хранение | Диск и буферный кэш | RAM с опциональной persistence |
| Запросы | SQL, JOIN, агрегации | Команды по ключам |
| Транзакции | ACID | Атомарные команды, MULTI/EXEC, Lua |
| Типичная роль | Источник истины | Кэш, сессии, счётчики, события |
| TTL | Не центральная функция | Встроенная возможность |
Типичная архитектура:
PostgreSQL - долговременные связанные данные
Redis - быстрые временные и производные данные
Типичные ошибки
- использовать Redis как бесконечную память;
- хранить критичные данные без persistence;
- считать Pub/Sub надёжной очередью;
- применять
KEYSв production; - хранить огромные значения;
- не ставить TTL временным данным;
- смешивать кэш, сессии и критичную очередь без изоляции;
- освобождать lock обычным
DEL; - считать replica резервной копией;
- добавлять Redis без измеренной проблемы.
Итог
Redis - сервер структур данных в памяти.
key - имя записи
data type - string, hash, list, set, sorted set, stream и другие
TTL - время жизни
eviction - удаление при нехватке памяти
persistence - RDB и AOF
replication - копирование на replica
Redis особенно полезен, когда задача выражается одной атомарной командой:
INCR - счётчик
SADD - уникальное множество
ZADD - рейтинг
SET NX EX - временный захват
XADD - событие в stream
После понимания PostgreSQL и Redis статья про Docker становится логичным продолжением: читатель уже знает, какие сервисы запускаются, почему PostgreSQL нужен volume и зачем Redis нужны лимиты памяти, TTL и сеть.