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 — один из тех инструментов, которые учишь за день, а потом используешь везде.