Как вы реализовывали кеширование в Node.js приложениях?

«Как вы реализовывали кеширование в Node.js приложениях?» — вопрос из категории Node.js Core, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Использовал несколько стратегий кеширования в Node.js в зависимости от требований:

1. In-memory кеш для быстрых операций:

class MemoryCache {
  constructor(ttl = 60000) {
    this.cache = new Map();
    this.ttl = ttl;
  }

  set(key, value, customTtl) {
    const expiry = Date.now() + (customTtl || this.ttl);
    this.cache.set(key, { value, expiry });

    // Автоочистка просроченных записей
    setTimeout(() => {
      if (this.cache.get(key)?.expiry < Date.now()) {
        this.cache.delete(key);
      }
    }, customTtl || this.ttl);
  }

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

// Использование
const cache = new MemoryCache();
const user = cache.get(`user:${userId}`) || await fetchUser(userId);

2. Redis для распределенного кеширования:

const redis = require('redis');
const { promisify } = require('util');

class RedisCache {
  constructor() {
    this.client = redis.createClient({
      url: process.env.REDIS_URL
    });
    this.getAsync = promisify(this.client.get).bind(this.client);
    this.setExAsync = promisify(this.client.setex).bind(this.client);
  }

  async getOrSet(key, fetchFn, ttl = 3600) {
    const cached = await this.getAsync(key);
    if (cached) return JSON.parse(cached);

    const data = await fetchFn();
    await this.setExAsync(key, ttl, JSON.stringify(data));
    return data;
  }
}

// Пример с кешированием запросов к БД
app.get('/api/products', async (req, res) => {
  const products = await redisCache.getOrSet(
    `products:page:${req.query.page}`,
    () => Product.find().skip(offset).limit(limit),
    300 // 5 минут
  );
  res.json(products);
});

3. HTTP-кеширование с заголовками:

app.get('/api/static-data', (req, res) => {
  const data = getStaticData();

  // Устанавливаем заголовки кеширования
  res.set({
    'Cache-Control': 'public, max-age=3600', // 1 час
    'ETag': generateETag(data),
    'Last-Modified': new Date().toUTCString()
  });

  // Проверяем условные запросы
  if (req.fresh) { // Используется middleware fresh
    return res.status(304).end(); // Not Modified
  }

  res.json(data);
});

4. Кеширование на уровне БД:

  • Query caching в Mongoose
  • Connection pooling для повторного использования соединений
  • DataLoader для batch-запросов в GraphQL

5. Стратегии инвалидации:

  • TTL-based (время жизни)
  • Write-through (обновление кеша при записи)
  • Cache-aside (ленивая загрузка)
  • Tag-based инвалидация для сложных зависимостей

Производительность:

  • In-memory: ~0.1ms на операцию
  • Redis: ~1-5ms (с учетом сетевой задержки)
  • Без кеша: 50-500ms (зависит от источника данных)

Для production-приложений обычно комбинирую несколько подходов: in-memory для горячих данных, Redis для распределенного доступа, HTTP-кеширование для статики.