Кэширование в JavaScript: Redis, Memcached и встроенные решения (продвинутый уровень)

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Кэширование в JavaScript: Redis, Memcached и встроенные решения (продвинутый уровень)

В современной веб-разработке производительность приложения часто упирается в скорость доступа к данным. Каждый лишний запрос к базе данных или внешнему API увеличивает время ответа и нагрузку на сервер. Кэширование — это не просто "сохранить результат", а целая наука с выбором правильной стратегии, инструмента и механизма инвалидации. В этой статье мы разберём продвинутые техники кэширования в Node.js: от встроенных решений до промышленных систем Redis и Memcached.

1. Встроенное кэширование в Node.js: простота и ограничения

Прежде чем подключать внешние сервисы, стоит оценить возможности самого Node.js. Встроенное кэширование реализуется с помощью объектов Map или WeakMap, а также простых глобальных переменных. Это идеально для данных, которые редко меняются и живут в рамках одного процесса.

// Простейший кэш в памяти с TTL (Time To Live)
class MemoryCache {
  constructor(ttl = 60000) {
    this.cache = new Map();
    this.ttl = ttl;
  }

  set(key, value) {
    this.cache.set(key, {
      value,
      expiry: Date.now() + this.ttl
    });
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;
    if (Date.now() > item.expiry) {
      this.cache.delete(key);
      return null;
    }
    return item.value;
  }

  delete(key) {
    this.cache.delete(key);
  }

  clear() {
    this.cache.clear();
  }
}

// Использование
const cache = new MemoryCache(30000); // 30 секунд TTL
cache.set('user:42', { name: 'Alice', role: 'admin' });
console.log(cache.get('user:42')); // { name: 'Alice', role: 'admin' }

Плюсы: нулевая задержка сети, простота реализации, не требует внешних зависимостей.
Минусы: данные теряются при перезапуске процесса, не масштабируется на несколько серверов, ограничено оперативной памятью одного узла.

Для микросервисов, работающих в одном процессе, или для кэширования результатов тяжелых вычислений (например, рендеринга шаблонов) встроенное решение — отличный выбор. Однако для распределённых систем его недостаточно.

2. Redis: король кэширования в Node.js

Redis — это in-memory хранилище данных, которое часто используется как высокопроизводительный кэш. Его главные преимущества: поддержка сложных структур данных (строки, списки, множества, хэши), встроенный TTL, pub/sub и персистентность на диск.

Подключение Redis в Node.js

Для работы с Redis в Node.js используется пакет ioredis (рекомендуется) или redis. Рассмотрим пример с ioredis:

const Redis = require('ioredis');

const redis = new Redis({
  host: 'localhost',
  port: 6379,
  retryStrategy: (times) => Math.min(times * 50, 2000) // стратегия повторного подключения
});

// Базовые операции
await redis.set('mykey', 'Hello, Redis!', 'EX', 60); // TTL 60 секунд
const value = await redis.get('mykey');
console.log(value); // "Hello, Redis!"

// Использование хэша для хранения сложных объектов
await redis.hmset('user:100', {
  name: 'Bob',
  email: 'bob@example.com',
  age: 30
});
const user = await redis.hgetall('user:100');
console.log(user); // { name: 'Bob', email: 'bob@example.com', age: '30' }

Продвинутые стратегии с Redis

Cache-Aside (Lazy Loading): приложение сначала проверяет кэш, и только при промахе обращается к базе данных и заполняет кэш.

async function getUser(userId) {
  const cacheKey = `user:${userId}`;
  let user = await redis.get(cacheKey);
  if (user) {
    return JSON.parse(user);
  }
  // Промах кэша — загружаем из БД
  user = await db.findUserById(userId);
  if (user) {
    await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); // кэш на 1 час
  }
  return user;
}

Write-Through и Write-Behind: запись данных всегда проходит через кэш. При Write-Through данные синхронно записываются в БД, при Write-Behind — асинхронно с задержкой. Это повышает консистентность, но требует аккуратной реализации.

Инвалидация кэша: одна из сложнейших задач. Используйте версионирование ключей (например, user:42:v2), установку короткого TTL для "горячих" данных или паттерн "публикация-подписка" для уведомления других сервисов об изменениях.

// Инвалидация через pub/sub
const subscriber = new Redis();
subscriber.subscribe('cache:invalidate');

subscriber.on('message', (channel, message) => {
  if (channel === 'cache:invalidate') {
    const key = message;
    redis.del(key);
    console.log(`Кэш инвалидирован: ${key}`);
  }
});

// Публикация при изменении данных
await redis.publish('cache:invalidate', 'user:42');

3. Memcached: легковесная альтернатива для простых сценариев

Memcached — это распределённая система кэширования, которая проще Redis, но уступает ему в функциональности. Memcached поддерживает только строковые значения, не имеет персистентности и сложных структур данных. Однако он быстрее в простых операциях get/set и потребляет меньше памяти на служебные нужды.

Работа с Memcached в Node.js

const memjs = require('memjs');
const mc = memjs.Client.create();

// Запись с TTL (в секундах)
await mc.set('mykey', 'Hello, Memcached!', { expires: 60 });

// Чтение
const { value } = await mc.get('mykey');
console.log(value ? value.toString() : null); // "Hello, Memcached!"

// Удаление
await mc.delete('mykey');

Когда выбирать Memcached? Если ваши данные — это простые строки (например, HTML-фрагменты, сериализованные JSON-объекты), и вам не нужны транзакции, pub/sub или персистентность. Memcached отлично подходит для кэширования результатов SQL-запросов или сессий в простых архитектурах.

Блоги

Рекомендаций по книгам