Skip to content
PПромтбук
RUEN
04Производительность

Стратегия кеширования

Где кешировать, на сколько, как инвалидировать — кеш который не лжёт.

Спроектируй кеширование.

Закон Phil Karlton: "Есть только две сложные вещи в Computer Science: cache invalidation and naming things."

Слои кеша

Browser → CDN → Reverse proxy → App → Memory cache → DB

Чем ближе к юзеру — тем быстрее. Чем дальше — тем актуальнее.

1. Browser cache

HTTP headers:

Cache-Control: public, max-age=31536000, immutable  # 1 год для assets
Cache-Control: public, max-age=0, must-revalidate    # для HTML (всегда проверять)
Cache-Control: private, no-store                     # для sensitive

ETag для conditional requests:

GET /api/users → 200 + ETag: "abc123"
GET /api/users + If-None-Match: "abc123" → 304 (без body)

2. CDN cache

Cloudflare / Vercel Edge / AWS CloudFront:

  • Static assets — cache forever (hash в имени файла)
  • HTML — cache минутами, поскольку часто меняется
  • API responses — selectively (с осторожностью)

Cache key

  • URL + query
  • Поскольку cookie / auth — обычно NO cache (private)
  • Локализация — отдельные ключи

3. App-level cache (in-memory)

Для:

  • Configurations
  • Hot computations
  • Static data
import LRU from 'lru-cache';
const cache = new LRU({ max: 1000, ttl: 60_000 });

function getUser(id) {
  if (cache.has(id)) return cache.get(id);
  const user = fetchFromDB(id);
  cache.set(id, user);
  return user;
}

Лимиты

  • Размер (max items)
  • TTL (time to live)
  • Memory (если объекты большие)

4. Distributed cache (Redis)

Для:

  • Cross-instance (несколько серверов)
  • Сессии
  • Rate limiting counters
  • Pre-computed aggregations
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);

const user = await db.findUser(id);
await redis.setex(`user:${id}`, 300, JSON.stringify(user));
return user;

5. DB query cache

Многие БД имеют свой:

  • PostgreSQL: pg_stat_statements (просто статистика)
  • MySQL: query cache (deprecated в 8.0)
  • Лучше: materialised views для тяжёлых aggregates

Стратегии invalidation

A. TTL (Time-To-Live)

Самое простое. Кеш сам истечёт.

Минусы:

  • До истечения — старые данные
  • Если данные часто меняются — много инвалидаций

B. Event-based

При update в DB → invalidate соответствующие ключи.

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

Минусы:

  • Можно забыть какой ключ инвалидировать
  • Сложно при много сервисах

C. Versioning

Ключ включает версию:

users:v123:list

При обновлении — увеличиваем версию. Старые ключи остаются (пока не expire), но не используются.

D. Cache-aside vs write-through

  • Cache-aside (lazy): app проверяет cache, грузит из DB на miss, заполняет cache. Простой.
  • Write-through: app пишет в cache + DB одновременно. Cache всегда актуален. Сложнее.

Что НЕ кешировать

  • Per-user данные в shared cache (privacy)
  • Случайные / некэшируемые ответы
  • Очень редкие запросы (не stoit memory)
  • То что должно быть real-time (stock prices)

Тонкости

Cache stampede

Когда кеш истёк → 1000 запросов одновременно лезут в DB.

Защита:

  • Lock первого, остальные ждут
  • Pre-warming перед истечением
  • Stale-while-revalidate

Cache pollution

Bot scraper заполнил cache редкими ключами → нужные вытеснены.

Защита:

  • LRU с приоритетами
  • Bot detection
  • Per-key TTL

Метрики

  • Hit rate (>80% хорошо, <50% плохо)
  • Miss rate
  • Eviction rate
  • Memory usage
  • Latency hit vs miss

Анти-паттерны

  • ❌ Кешировать всё подряд (бесполезно тратит memory)
  • ❌ TTL бесконечный без invalidation
  • ❌ Per-user данные в global cache → leak
  • ❌ Не учитывать stale data (юзер видит старое)
  • ❌ Cache в frontend без указания версии

Чек-лист

  • Cache headers на статике
  • CDN перед app server
  • In-memory cache для hot reads
  • Redis для cross-instance
  • Invalidation strategy выбрана
  • Hit rate отслеживается
  • Stampede protection
Похожие промты