Redis появился в 2009 году и с тех пор стал стандартом для задач, где нужна скорость. Не реляционная база с индексами и транзакциями, не файловое хранилище — а быстрое хранилище данных в памяти, которое решает конкретные проблемы.
Что такое Redis и зачем он нужен
Redis — это key-value хранилище, которое держит данные в оперативной памяти. Поэтому работает быстро: типичная задержка — меньше 1 миллисекунды. PostgreSQL при сложном запросе может думать десятки и сотни миллисекунд. Redis отдаёт данные за 0,1–0,5 мс.
Основные сценарии:
- Кеширование результатов запросов к БД
- Хранение сессий пользователей
- Очереди задач (job queues)
- Pub/Sub — публикация и подписка на события
- Rate limiting — ограничение количества запросов
- Счётчики и лидерборды
Redis поддерживает разные структуры данных: строки, хэши, списки, множества, sorted sets, streams. Это не просто «положи строку — забери строку». Благодаря этому Redis решает целый класс задач, под которые раньше писали специализированные системы.
Кеширование: как это работает
Классический сценарий: у вас есть страница с каталогом товаров. При каждом запросе вы идёте в PostgreSQL, джойните 5 таблиц, агрегируете данные — это занимает 300 мс. При 100 одновременных пользователях база начинает задыхаться.
Решение простое: первый запрос идёт в базу, результат сохраняется в Redis. Все последующие запросы — из Redis за 1 мс. База отдыхает.
import redis
import json
r = redis.Redis(host='localhost', port=6379, db=0)
def get_catalog(category_id):
cache_key = f"catalog:{category_id}"
cached = r.get(cache_key)
if cached:
return json.loads(cached)
result = db.query(
"SELECT ... FROM products WHERE category_id = %s",
category_id
)
r.setex(cache_key, 300, json.dumps(result))
return result
TTL (time to live) — ключевой параметр. Если не выставить время жизни ключа, данные будут жить в Redis вечно, пока не кончится память. Для каталога 5 минут — разумно. Для курса валют — 60 секунд. Для профиля пользователя — 30 минут или до момента, когда пользователь что-то изменил.
Инвалидация кеша
Самая сложная часть кеширования — не само кеширование, а инвалидация. Когда пользователь обновил профиль, старые данные из кеша нужно удалить.
Простой подход: удалять ключ при изменении данных.
def update_user_profile(user_id, data):
db.update("UPDATE users SET ... WHERE id = %s", user_id, data)
r.delete(f"user:{user_id}")
Следующий запрос к профилю пойдёт в базу и снова заполнит кеш.
Cache stampede
Есть неочевидная проблема: если 1000 пользователей одновременно запросили данные, кеш которых только что протух — все 1000 запросов полетят в базу одновременно. База падает.
Решение — блокировка на время регенерации кеша:
def get_with_lock(key, ttl, generate_fn):
cached = r.get(key)
if cached:
return json.loads(cached)
lock_key = f"{key}:lock"
if r.set(lock_key, 1, nx=True, ex=10):
result = generate_fn()
r.setex(key, ttl, json.dumps(result))
r.delete(lock_key)
return result
else:
time.sleep(0.1)
return get_with_lock(key, ttl, generate_fn)
Сессии пользователей
Redis отлично подходит для хранения сессий. Стандартный подход с хранением в реляционной базе плохо масштабируется: при каждом запросе нужно ходить в БД для проверки.
С Redis сессия хранится как хэш:
def create_session(user_id, user_data):
session_id = str(uuid.uuid4())
session_key = f"session:{session_id}"
r.hset(session_key, mapping={
"user_id": user_id,
"email": user_data["email"],
"role": user_data["role"],
"created_at": int(time.time())
})
r.expire(session_key, 86400) # 24 часа
return session_id
def get_session(session_id):
return r.hgetall(f"session:{session_id}")
Время чтения сессии — меньше миллисекунды. При горизонтальном масштабировании все серверы приложения читают сессии из одного Redis. Никаких проблем со sticky sessions.
Rate limiting
Ограничение количества запросов — типичная задача для API. Нужно разрешать не больше 100 запросов в минуту с одного IP.
Redis решает это через инкремент счётчика:
def check_rate_limit(ip, limit=100, window=60):
key = f"ratelimit:{ip}:{int(time.time() // window)}"
current = r.incr(key)
if current == 1:
r.expire(key, window)
return current <= limit
Каждую минуту создаётся новый ключ с временным суффиксом. Когда минута заканчивается — ключ устаревает и счётчик обнуляется. Работает атомарно, без race conditions.
Очереди задач
Вторая большая задача Redis — очереди. Нужно отправить 10 000 писем? Сгенерировать PDF-отчёты для 500 пользователей? Сделать ресайз загруженных изображений? Всё это не нужно делать синхронно в рамках HTTP-запроса.
Пользователь нажал «Отправить» — вы положили задачу в очередь и ответили «Принято». Воркер в фоне забрал задачу и обработал.
Простая очередь на Lists
# Продюсер
def enqueue_email(to, subject, body):
task = json.dumps({"to": to, "subject": subject, "body": body})
r.lpush("email_queue", task)
# Воркер
def worker():
while True:
# BRPOP блокируется, если очередь пуста — не жрёт CPU
_, task_data = r.brpop("email_queue")
task = json.loads(task_data)
send_email(task["to"], task["subject"], task["body"])
BRPOP — блокирующая операция. Воркер ждёт, пока в очереди появятся задачи. Не крутится в цикле, не нагружает CPU.
Celery + Redis
На практике очереди редко пишут с нуля. В Python-экосистеме стандарт — Celery с Redis в качестве брокера.
from celery import Celery
app = Celery('tasks', broker='redis://localhost:6379/0')
@app.task
def send_report(user_id):
pdf = generate_pdf(user_id)
save_and_notify(user_id, pdf)
# В коде приложения:
send_report.delay(user_id=123)
Задача встаёт в очередь и выполняется асинхронно. Можно запустить несколько воркеров на разных машинах — они будут конкурентно разбирать задачи из одной очереди.
Celery поддерживает несколько очередей с разным приоритетом и задержки:
# Срочная задача — в приоритетную очередь
send_notification.apply_async(args=[user_id], queue='high_priority')
# Выполнить через 10 минут
send_reminder.apply_async(args=[user_id], countdown=600)
Redis Streams — когда нужна история
Обычная очередь на Lists — это «один раз прочитал, задача ушла». Если воркер упал во время обработки — задача потеряна.
Redis Streams решают это: данные не удаляются при чтении, каждая consumer group читает свою позицию в стриме.
# Добавляем событие
r.xadd("events", {
"type": "order_created",
"order_id": "12345",
"user_id": "67890"
})
# Consumer group читает события
r.xgroup_create("events", "notifications_service", id="0", mkstream=True)
while True:
messages = r.xreadgroup(
"notifications_service", "worker_1",
{"events": ">"}, count=10, block=5000
)
for stream, msgs in messages:
for msg_id, data in msgs:
process_event(data)
r.xack("events", "notifications_service", msg_id)
Если воркер упал без подтверждения — сообщение останется в pending list и другой воркер заберёт его.
Настройка Redis на продакшне
Несколько вещей, которые нельзя игнорировать.
Persistence. По умолчанию Redis хранит данные в памяти. При перезагрузке — всё теряется. Для продакшна нужно включить RDB или AOF:
# redis.conf
save 900 1
save 300 10
appendonly yes
Maxmemory. Обязательно ставьте лимит памяти и политику вытеснения:
maxmemory 2gb
maxmemory-policy allkeys-lru
allkeys-lru — при нехватке памяти вытесняет наименее используемые ключи. Для кеша это то, что нужно.
Аутентификация. Redis по умолчанию слушает без пароля. В продакшне — всегда пароль:
requirepass your_strong_password
И никогда не открывайте порт 6379 в интернет.
Репликация. Один экземпляр Redis — точка отказа. Для высокой доступности — Redis Sentinel или Redis Cluster.
Мониторинг
Полезные команды:
# Общая статистика
redis-cli INFO
# Медленные запросы
redis-cli SLOWLOG GET 10
# Использование памяти по ключу
redis-cli MEMORY USAGE key_name
Ключевые метрики: used_memory, connected_clients, keyspace_hits и keyspace_misses. Отношение hits/(hits+misses) — это hit rate кеша. Нормальный показатель — выше 90%.
На проектах, которые выходят из REEXY, Redis появляется практически в каждом серьёзном стеке: без него масштабирование упирается в базу данных раньше, чем успеваешь понять почему.
Когда Redis не нужен
Redis решает конкретные задачи — не нужно пихать его везде.
Если у вас 100 запросов в день — базы данных хватит с головой. Если нужна надёжная доставка с сложной маршрутизацией — RabbitMQ справится лучше. Если объём данных для кеша превышает доступную RAM — Redis станет дорогим удовольствием.
Добавляйте Redis тогда, когда видите конкретную проблему: база тормозит под нагрузкой, нужно вынести фоновые задачи из синхронного потока, требуется real-time функциональность.
Быстрый старт
Локально Redis запускается за минуту:
# docker-compose.yml
services:
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
volumes:
redis_data:
Для Python: pip install redis. Для Node.js: npm install ioredis. Для Go: go get github.com/redis/go-redis/v9.
Порог входа низкий, отдача — высокая. Redis — один из тех инструментов, которые учишь за день, а потом используешь везде.