Как вы реализовывали кеширование в 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-кеширование для статики.

Ответ 18+ 🔞

А, ну вот, слушай, про кеширование в Node.js. Тема, блядь, важная, как соль в борще — без неё можно, но потом охуеешь от того, как всё медленно работает. Расскажу, как я обычно эту магию делаю, чтобы сервер не ложился как последний бздун при первой же нагрузке.

1. Кеш прямо в оперативке — для самых отчаянных. Это когда тебе надо, чтобы всё было быстрее, чем мысль «а не выпить ли?». Просто, как три копейки, но есть нюанс — память жрёт, и если сервер перезапустишь, всё накрывается медным тазом.

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);
});

Если данные не поменялись, сервер просто шлёт статус «304 — не трогай старое», и всё. Трафик экономится, нагрузка падает. Просто, но гениально, ёпта.

4. Кеширование на уровне самой базы данных. Тут уже без твоего прямого участия, но знать надо. В Mongoose есть встроенные механизмы, Connection pooling не даёт открывать новое соединение на каждый чих, а для GraphQL есть DataLoader, который кучу мелких запросов собирает в один здоровенный. Волнение ебать, когда видишь, как это работает.

5. А самое главное — как это всё обновлять и когда сбрасывать. Вот тут уже надо думать головой, а не жопой. Стратегий куча:

  • По времени (TTL) — проще пареной репы, поставил и забыл. Но данные могут устареть раньше.
  • Write-through — обновляешь базу, заодно и кеш. Надёжно, но медленнее на запись.
  • Cache-aside (Lazy Loading) — классика, которую все используют. Нет в кеше — пошёл взял из базы, положил в кеш. Есть риск «кеш-промаха» при первом запросе.
  • Инвалидация по тегам — вот это для сложных зависимостей, когда данные связаны. Удалил товар — инвалидируешь все списки, где он фигурировал. Терпения ноль ебать настраивать, но потом живёшь как король.

Итог по скорости, чтобы ты понимал масштаб:

  • Прямо в памяти: ~0.1 мс — быстрее, чем ты успел подумать.
  • Redis: ~1-5 мс — с учётом того, что надо по сети сходить. Но всё равно в десятки раз быстрее базы.
  • Без кеша вообще: 50-500 мс — и это в лучшем случае. А если база тупит, то и секунды.

Так что, чувак, в продакшене я обычно всё комбинирую. Что-то горячее — в память, что нужно между серверами — в Redis, а статику вообще браузерам кешировать велю. Главное — не переусердствовать, а то можно так закешировать, что потом сам не разберёшь, где актуальные данные, а где вчерашние.