Урок 05

Redis: когда база данных — это кэш

Redis не БД

Redis — in-memory хранилище. Может использоваться как БД, но обычно — как кэш.

Ключевое:

  • Данные в RAM.
  • Persistence — опционально.
  • Потеря данных при сбое — приемлемо для кэша.
  • Микросекундные операции.

Правило: Redis — для данных, которые можно пересчитать.

Когда Redis как кэш

Классические сценарии:

  • Результаты тяжёлых SQL.
  • Внешние API.
  • Настройки, справочники.
  • HTML-фрагменты.
  • Сессии.
  • Rate limiting.
  • Счётчики.
  • Очереди.

Cache-Aside

Основной паттерн:

$user = Cache::remember("user:{$id}", 3600, function () use ($id) {
    return User::findOrFail($id);
});

Что происходит:

  1. Проверяем Redis.
  2. Есть — отдаём.
  3. Нет — идём в БД.
  4. Кладём в Redis.
  5. Отдаём.

Инвалидация

При записи:

$user->update($data);
Cache::forget("user:{$user->id}");

Теги:

Cache::tags(['users'])->put("user:{$id}", $user, 3600);
Cache::tags(['users'])->flush();

Версионирование:

$version = Cache::get('users:version', 1);
$key = "user:{$id}:v{$version}";

// инвалидация
Cache::increment('users:version');

Правило: TTL + явная инвалидация при записи.

TTL — обязательно

Cache::put('key', $value, 3600);

Почему TTL:

  • Автоочистка.
  • Ограничение устаревших.
  • RAM не уходит.

Правило: всегда TTL. Исключения — справочники с явной инвалидацией.

Что кэшировать

Хорошо:

  • Тяжёлые запросы с JOIN.
  • Внешние API.
  • Справочники.
  • Топ-10 товаров.
  • Профиль пользователя.
  • Настройки.

Плохо:

  • Данные, которые меняются каждую секунду.
  • Персонально чувствительные данные (пароли, токены).
  • Много уникальных ключей (не будут попадать в cache).
  • Транзакционные данные.

Что НЕ кэшировать

Деньги:

Баланс в Redis — рассинхрон

Транзакции:

Заказ в Redis → потеря при рестарте

Данные с частой записью:

Постоянная инвалидация — нет смысла в кэше

Правило: кэш — для данных, которые часто читают, редко пишут.

Кэш на уровне приложения

Laravel:

Cache::remember('users:list', 3600, fn () => User::all());
Cache::forget('users:list');
Cache::tags(['users'])->flush();

Node.js:

async function getUser(id) {
    const cached = await redis.get(`user:${id}`);
    if (cached) return JSON.parse(cached);

    const user = await db.users.find(id);
    await redis.setex(`user:${id}`, 3600, JSON.stringify(user));
    return user;
}

Python:

import json
import redis

r = redis.Redis()

cached = r.get(f'user:{user_id}')
if cached:
    return json.loads(cached)

user = db.users.find(user_id)
r.setex(f'user:{user_id}', 3600, json.dumps(user))
return user

Сессии

Идея: сессия в Redis вместо файлов/БД.

Laravel:

SESSION_DRIVER=redis

Node.js (express-session):

import session from 'express-session';
import RedisStore from 'connect-redis';

app.use(session({
    store: new RedisStore({ client: redis }),
    secret: 'xxx',
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 86400 * 1000 },
}));

Что даёт:

  • Быстрое чтение.
  • Общие сессии между инстансами.
  • Автоочистка по TTL.

Очереди

Redis — драйвер для Laravel Queue, BullMQ.

Laravel:

QUEUE_CONNECTION=redis

Node.js (BullMQ):

import { Queue, Worker } from 'bullmq';

const queue = new Queue('emails', { connection: redis });
await queue.add('welcome', { userId: 1 });

new Worker('emails', async (job) => {
    await sendEmail(job.data);
}, { connection: redis });

Что важно:

  • Persistence — иначе задачи теряются при рестарте.
  • AOF для надёжности.
  • Horizon (Laravel) или Bull Board для UI.

Rate limiting

Простой счётчик:

INCR rate:user:5
EXPIRE rate:user:5 60

Sliding window:

ZADD rate:user:5 <timestamp> <request_id>
ZREMRANGEBYSCORE rate:user:5 0 <timestamp - window>
ZCARD rate:user:5

Laravel:

RateLimiter::for('api', fn ($request) =>
    Limit::perMinute(60)->by($request->user()?->id)
);

Distributed locks

Простой lock:

SET lock:job:5 abc NX EX 30

Что даёт: только один инстанс возьмёт lock.

Laravel:

Cache::lock('job:' . $id, 30)->get(function () {
    // критическая секция
});

Node.js (redlock):

import Redlock from 'redlock';

const redlock = new Redlock([redis], { retryCount: 0 });
const lock = await redlock.acquire(['lock:job:5'], 30000);

try {
    // критическая секция
} finally {
    await lock.release();
}

Счётчики и лимиты

Просмотры:

INCR post:5:views

Онлайн-пользователи:

SADD online:users user5
SREM online:users user5
SCARD online:users

Реакции:

HSET post:5:reactions likes 10 hearts 5
HINCRBY post:5:reactions likes 1

Что даёт: быстрые счётчики без записи в БД. Периодически — sync в БД.

Pub/Sub

PUBLISH notifications '{"userId": 5, "msg": "hi"}'
SUBSCRIBE notifications

Что даёт: real-time сообщения.

Пример — Laravel → Node.js:

Redis::publish('app:events', json_encode([
    'channel' => "user:{$userId}",
    'event' => 'order.created',
    'data' => $data,
]));
sub.subscribe('app:events');
sub.on('message', (ch, msg) => {
    const { channel, event, data } = JSON.parse(msg);
    io.to(channel).emit(event, data);
});

Что даёт: мост между сервисами без прямой связи.

Ошибка Redis

Проблема: Redis упал. Приложение падает без кэша?

Решение: graceful degradation.

try {
    $user = Cache::remember('user:' . $id, 3600, fn () => User::find($id));
} catch (\Throwable $e) {
    // Redis недоступен — читаем напрямую
    Log::warning('Cache unavailable', ['error' => $e->getMessage()]);
    $user = User::find($id);
}

Правило: кэш не должен ломать приложение при сбое.

Метрики

INFO stats

Что смотреть:

  • keyspace_hits, keyspace_misses — hit rate.
  • evicted_keys — вытеснение.
  • used_memory — RAM.
  • connected_clients.
  • instantaneous_ops_per_sec.

Hit rate: > 90%.

Persistence

RDB — snapshot:

save 900 1
save 300 10
save 60 10000

AOF — append-only file:

appendonly yes
appendfsync everysec

Правило:

  • Чистый кэш — persistence не нужен.
  • Сессии, очереди — AOF.
  • Критичные данные — Redis не подходит как primary.

HA и Cluster

Sentinel:

sentinel monitor mymaster 10.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000

Cluster:

redis-cli --cluster create host1:6379 host2:6379 host3:6379

Правило:

  • Кэш — обычный Redis с Sentinel.
  • Большие данные — Cluster.

Maxmemory

maxmemory 2gb
maxmemory-policy allkeys-lru

Для кэша: allkeys-lru — вытесняет давно неиспользуемые.

Для очередей: noeviction — не терять задачи.

Правило: разные инстансы для кэша и очередей, чтобы не вытесняли друг друга.

Безопасность

Что делать:

  • requirepass — пароль.
  • bind 127.0.0.1 — только локально.
  • Firewall — не открывать 6379.
  • rename-command для опасных команд.

⚠️ Публичный Redis — массово взламывают (майнеры, шифрование).

Когда Redis НЕ кэш

Иногда Redis — primary:

  • Счётчики просмотров — можно жить с потерей.
  • Онлайн-статусы — пересчитываются.
  • Leaderboards — можно пересчитать.
  • Очереди — Redis как брокер.
  • Pub/Sub — real-time.

Правило: если данные можно восстановить из основного источника — Redis ок как primary для них.

Частые ошибки

1. Redis как единственная БД:

Рестарт → потеря

Решение: Postgres для источника правды.

2. Кэш без TTL:

RAM кончается

Решение: всегда TTL.

3. Кэш без инвалидации:

Старые данные

Решение: forget при записи.

4. KEYS * в проде:

Блокирует Redis

Решение: SCAN.

5. Публичный Redis:

Взлом

Решение: firewall + пароль.

6. Redis для очередей без AOF:

Задачи теряются

Решение: AOF.

7. Кэш ломает приложение:

Redis недоступен → 500

Решение: graceful degradation.

8. Один Redis для всего:

Кэш вытесняет задачи

Решение: разные инстансы / БД.

Практика

1. Настройте Redis для Laravel/Node.

2. Кэшируйте запрос к БД — замерьте разницу.

3. Настройте TTL и инвалидацию.

4. Rate limiting через INCR+EXPIRE.

5. Distributed lock через SET NX EX.

6. Сессии в Redis.

7. Очередь в Redis.

8. Метрики hit rate.

Итог

  • Redis — кэш, сессии, очереди, счётчики, locks, pub/sub.
  • Cache-Aside — основной паттерн.
  • TTL + инвалидация — обязательно.
  • Persistence — для очередей/сессий, не для кэша.
  • HA — Sentinel.
  • Maxmemory — allkeys-lru для кэша.
  • Безопасность — firewall, пароль.
  • Не primary для критичных данных.
  • Graceful degradation — обязательно.
  • Метрики — hit rate > 90%.

Дальше — кэширование: стратегии и паттерны.