← Назад к вопросам

Как реализовать кэширование в Node.js приложении?

2.2 Middle🔥 171 комментариев
#Node.js и JavaScript#Кэширование и производительность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Кэширование в Node.js приложении

Кэширование - это сохранение результатов дорогих операций для повторного использования. Правильное кэширование может снизить нагрузку на БД в 10-100 раз и уменьшить время ответа с сотен миллисекунд до единиц.

Уровни кэширования

Client Cache -> CDN -> Reverse Proxy -> App Cache -> DB Cache
(браузер)     (CF)   (nginx)          (Redis)      (pg buffer)

In-Memory кэш (в памяти процесса)

Самый быстрый, но не разделяется между инстансами:

import NodeCache from "node-cache";

const cache = new NodeCache({
  stdTTL: 300,
  checkperiod: 60,
  maxKeys: 10000
});

async function getUser(id: string): Promise<User> {
  const cacheKey = `user:${id}`;

  const cached = cache.get<User>(cacheKey);
  if (cached) return cached;

  const user = await db.query("SELECT * FROM users WHERE id = $1", [id]);
  cache.set(cacheKey, user, 600);
  return user;
}

Redis кэш (распределенный)

Работает между инстансами, переживает перезапуски:

import Redis from "ioredis";

const redis = new Redis({
  host: "localhost",
  port: 6379,
  maxRetriesPerRequest: 3
});

async function getUserCached(id: string): Promise<User> {
  const cacheKey = `user:${id}`;

  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await userRepository.findById(id);
  await redis.setex(cacheKey, 600, JSON.stringify(user));
  return user;
}

Паттерн Cache-Aside (Lazy Loading)

Самый распространенный паттерн:

async function getCachedData<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number = 300
): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const data = await fetcher();
  await redis.setex(key, ttl, JSON.stringify(data));
  return data;
}

// Использование
const stats = await getCachedData(
  "dashboard:stats",
  () => analyticsService.calculateStats(),
  60
);

Инвалидация кэша

1. TTL (Time-To-Live):

await redis.setex("key", 300, value); // автоудаление через 5 минут

2. Ручная инвалидация при изменении:

async function updateUser(id: string, data: UpdateUserDto) {
  await userRepository.update(id, data);
  await redis.del(`user:${id}`);
  await redis.del("users:list");
}

3. Паттерн с тегами:

async function invalidateUserCache(userId: string) {
  const keys = await redis.keys(`user:${userId}:*`);
  if (keys.length > 0) {
    await redis.del(...keys);
  }
}

HTTP кэширование

app.get("/api/professions", async (req, res) => {
  const professions = await getCachedData("professions", fetchProfessions, 3600);

  res.set({
    "Cache-Control": "public, max-age=3600",
    "ETag": generateETag(professions)
  });
  res.json(professions);
});

// Conditional requests
app.get("/api/users/:id", async (req, res) => {
  const user = await getUser(req.params.id);
  const etag = generateETag(user);

  if (req.headers["if-none-match"] === etag) {
    return res.status(304).end();
  }

  res.set("ETag", etag);
  res.json(user);
});

Защита от Cache Stampede

Когда кэш истекает и 1000 запросов одновременно идут в БД:

const locks = new Map<string, Promise<unknown>>();

async function getCachedWithLock<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number
): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  if (!locks.has(key)) {
    locks.set(key, fetcher().then(async (data) => {
      await redis.setex(key, ttl, JSON.stringify(data));
      locks.delete(key);
      return data;
    }));
  }

  return locks.get(key) as Promise<T>;
}

Когда НЕ кэшировать

  • Данные, которые меняются при каждом запросе
  • Персональные/конфиденциальные данные (без строгого контроля)
  • Данные, где устаревание критично (финансовые операции)
  • Очень маленькие и быстрые запросы (overhead кэша больше выгоды)