Redis: кеширование

Redis — in-memory хранилище данных типа ключ-значение, используемое в Node.js-приложениях для кеширования результатов запросов, хранения сессий, rate-limiting и как брокер очередей.

Зачем нужно

Обращение к базе данных при каждом запросе замедляет API. Redis хранит данные в памяти, обеспечивая доступ за микросекунды. Кеширование популярных запросов (каталог товаров, профиль пользователя) снижает нагрузку на БД на порядки и уменьшает latency с десятков миллисекунд до долей.

Где используется

  • Кеширование результатов дорогих SQL-запросов
  • Хранение HTTP-сессий (альтернатива in-memory сессиям)
  • Rate-limiting запросов (счётчики с TTL)
  • Pub/Sub для real-time уведомлений
  • Хранение временных данных: OTP-коды, токены сброса пароля

Основной контент

Установка (ioredis)

npm install ioredis
# ioredis — рекомендуемый клиент: поддерживает Cluster, Sentinel, pipeline
// db/redis.js
const Redis = require('ioredis');

const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: process.env.REDIS_PORT || 6379,
  password: process.env.REDIS_PASSWORD,
  retryStrategy: (times) => Math.min(times * 50, 2000) // переподключение
});

redis.on('error', (err) => console.error('Redis error:', err));
redis.on('connect', () => console.log('Redis connected'));

module.exports = redis;

Базовые операции

const redis = require('./db/redis');

// Установить значение с TTL (seconds)
await redis.set('key', 'value', 'EX', 3600); // истечёт через 1 час

// Получить значение
const value = await redis.get('key'); // null если не найдено

// Хешировать объект
await redis.hset('user:1', { name: 'Alice', email: 'alice@example.com' });
const user = await redis.hgetall('user:1'); // { name: 'Alice', email: '...' }

// Удалить ключ
await redis.del('key');

// Проверить существование
const exists = await redis.exists('key'); // 0 или 1

// Время жизни ключа
const ttl = await redis.ttl('key'); // секунды, -1 = нет TTL, -2 = не существует

Паттерн Cache-Aside

// middleware/cache.js
const redis = require('../db/redis');

function cacheMiddleware(keyFn, ttl = 60) {
  return async (req, res, next) => {
    const key = keyFn(req);
    try {
      const cached = await redis.get(key);
      if (cached) {
        return res.json(JSON.parse(cached));
      }
    } catch (err) {
      console.error('Cache read error:', err);
    }

    // Перехватываем res.json() для сохранения в кеш
    const originalJson = res.json().bind(res);
    res.json() = async (data) => {
      try {
        await redis.set(key, JSON.stringify(data), 'EX', ttl);
      } catch (err) {
        console.error('Cache write error:', err);
      }
      return originalJson(data);
    };

    next;
  };
}

// Использование
router.get('/products',
  cacheMiddleware((req) => `products:${JSON.stringify(req.query)}`, 300),
  ProductController.getAll
);

Rate Limiting через Redis

// middleware/rateLimit.js
async function redisRateLimit(req, res, next) {
  const key = `rate:${req.ip}`;
  const limit = 100;
  const window = 60; // секунд

  const requests = await redis.incr(key);
  if (requests === 1) {
    await redis.expire(key, window); // установить TTL только при первом запросе
  }

  if (requests > limit) {
    return res.status(429).json({ error: 'Too many requests' });
  }

  res.setHeader('X-RateLimit-Remaining', limit - requests);
  next;
}

Хранение сессий

npm install connect-redis express-session
const session = require('express-session');
const { createClient } = require('redis');
const RedisStore = require('connect-redis').default;

const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect;

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 86400000 } // 1 день
}));

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

  • Не устанавливать TTL — ключи накапливаются и заполняют память; всегда указывать EX или PX
  • Кешировать ошибки — если запрос к БД вернул ошибку и она закешировалась, пользователи будут получать ошибку до истечения TTL; кешировать только успешные результаты
  • Не обрабатывать недоступность Redis — если Redis упал, не отдавать ошибку пользователю; обернуть в try/catch и продолжать работу без кеша
  • JSON.parse без проверкиredis.get может вернуть null; JSON.parse(null) выбросит ошибку

Связанные темы

Ресурсы