Legan Studio
Все статьи
~ 7 мин чтения

Кеширование на сайте: Redis, CDN, in-memory

Послойный кеш для сайта: HTTP cache, CDN, Redis, in-memory, инвалидация, stale-while-revalidate, тонкости с авторизованными страницами.

  • веб
  • разработка
  • производительность

«Сайт тормозит — поставим Redis» — фраза, после которой проблема не исчезает, а превращается в две: и тормозит, и в Redis несоответствующие данные. Кеш — мощнейший инструмент производительности, но без понимания слоёв и инвалидации он создаёт больше боли, чем решает.

Разберём, что и где кешируется и как не выстрелить себе в ногу.

Слои кеша

Сверху вниз — от ближайшего к пользователю до бэкенда:

СлойГде живётTTLЧто хранит
Browser cacheв браузередни-месяцыстатика (JS, CSS, картинки), HTML с правильными заголовками
CDN edgeгеографически близкий узелчасы-днито же + динамика с короткими TTL
Reverse proxy (nginx/Varnish)перед приложениемминуты-часыfull-page cache, fragment cache
In-memory приложенияв RAM процессасекунды-минутыочень горячие данные, конфиг
Redis / Memcachedотдельный серверсекунды-суткисессии, JSON-ответы API, результаты сложных запросов
БД query cacheвнутри БДсекундыодинаковые запросы (Postgres сам не кеширует, а вот Redis перед БД — да)

Главное правило: чем выше слой, тем дешевле hit и тем сложнее инвалидация. Кешировать в браузере на год — почти бесплатно, но обновить нельзя. Кешировать в Redis на минуту — точное управление, но каждый запрос идёт до приложения.

Browser cache — самый дешёвый

Статика с хешем в имени:

/assets/main.a8f3b9c.js
/assets/main.a8f3b9c.css
/_next/static/chunks/page-7f2e1a.js
location /_next/static {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

immutable — браузер не будет даже отправлять conditional request, тупо берёт из кеша. Next.js, Vite, webpack — все генерят имена с хешем по умолчанию. При деплое имя меняется, браузер скачивает новый файл.

Для HTML — никогда не immutable. Обычно Cache-Control: public, max-age=0, must-revalidate или s-maxage=60, stale-while-revalidate=300.

CDN

Cloudflare, BunnyCDN, Yandex CDN, Selectel CDN, VK Cloud CDN. Кладёт ваш сайт в десятки точек по миру (в РФ — региональные узлы).

Что кеширует CDN из коробки:

  • Картинки, видео, шрифты — TTL месяцы.
  • JS/CSS с хешем — TTL год.
  • Документы — короткий TTL или not cached.

HTML страниц — обычно нет, кроме случаев публичных кешированных страниц (блог, лендинг). Включается заголовком s-maxage:

// Next.js
export const revalidate = 3600; // ISR на час

// или
return new Response(html, {
  headers: { "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400" },
});

s-maxage — для CDN, max-age — для браузера. Часто различаются.

stale-while-revalidate

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

Cache-Control: public, s-maxage=60, stale-while-revalidate=86400

Свежие 60 секунд — отдаём из кеша. От 60 секунд до 24 часов — отдаём старую и параллельно пересобираем. Через 24 часа — уже честно ждём.

Идеально для блогов, новостей, каталогов. Не подходит для платежей и личных кабинетов.

Reverse proxy и full-page cache

nginx умеет кешировать full HTML за вас:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=site:10m max_size=1g
                 inactive=60m use_temp_path=off;

server {
  location / {
    proxy_cache site;
    proxy_cache_valid 200 1m;
    proxy_cache_use_stale error timeout updating;
    proxy_cache_bypass $cookie_session;
    proxy_pass http://backend;
  }
}

proxy_cache_bypass $cookie_session — авторизованным пользователям не отдавать кеш (у них своя версия). Без этого утечка данных.

In-memory кеш приложения

Самое быстрое, но локально на процесс. Для конфигов, справочников, вычисляемых редко-меняющихся данных:

import LRU from "lru-cache";
const cache = new LRU<string, any>({ max: 1000, ttl: 60_000 });

async function getCategoryTree() {
  const cached = cache.get("categories");
  if (cached) return cached;
  const data = await db.category.findMany();
  cache.set("categories", data);
  return data;
}

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

Redis — основной кеш-слой

Один Redis на всё приложение. Все процессы пишут и читают из общего пула.

import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);

async function getProduct(id: number) {
  const key = `product:${id}`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
  const product = await db.product.findUnique({ where: { id } });
  await redis.set(key, JSON.stringify(product), "EX", 300);
  return product;
}

Шаблон cache-aside: сначала проверяем кеш, при промахе идём в БД, кладём в кеш.

Альтернатива — write-through: при UPDATE сразу пишем и в БД, и в кеш. Сложнее, но всегда актуально.

Инвалидация — самое сложное

«There are only two hard things in Computer Science: cache invalidation and naming things.»

При изменении данных нужно сбросить кеш. Варианты:

  1. TTL: ничего не сбрасываем, ждём истечения. Простой, но устаревшие данные на TTL секунд.
  2. Явная инвалидация: после UPDATE делаем DEL нужного ключа. Сложно с зависимостями («поменялась цена — а сколько кешей затронуто?»).
  3. Tag-based (next.js, упрощённо): помечаем кеш тегами, инвалидируем по тегу.
// Next.js fetch
const data = await fetch(url, { next: { tags: ["products"], revalidate: 3600 } });

// при мутации
import { revalidateTag } from "next/cache";
revalidateTag("products");
  1. Pub/Sub: при изменении публикуем событие, все процессы стирают свои in-memory кеши.

Что не кешировать

  • Личные данные пользователя (личный кабинет, корзина) — нельзя класть в shared CDN-кеш.
  • Платежные операции — никаких stale.
  • Realtime-данные (биржа, чат) — кеш убивает смысл.
  • Запросы, где ответ зависит от 10+ переменных (юзер, сегмент, регион) — ключей будет миллион, hit rate низкий.

Что обязательно кешировать

  • Главная страница, лендинги — короткий TTL + ISR.
  • Каталог товаров — Redis кеш на 5-15 минут.
  • Карточки товаров без персональных скидок — на 30-60 минут.
  • Меню, навигация, конфиги магазина — in-memory на 5 минут.
  • Дорогие агрегации (топ продаж, рейтинг) — Redis на час.
  • API внешних сервисов (геокодер, курсы валют) — Redis на сутки.

Метрики

Обязательно измеряйте:

  • Hit rate — процент попаданий. Меньше 70% — кеш бесполезен.
  • Latency hit vs miss — насколько ускоряет.
  • Размер кеша — не вылезает ли за лимиты Redis.
  • Eviction rate — как часто Redis выкидывает данные из-за нехватки RAM.

В Grafana — дашборд по этим метрикам. Без них вы не знаете, работает ли вообще ваш кеш.

Тонкости с куки

Если HTML кешируется в CDN, но у пользователя есть Cookie: session=xxx — отдать одинаковый HTML двум разным пользователям нельзя. Решения:

  • Cookie не влияют на HTML (страница одинакова для всех) — кешируйте.
  • Cookie разделяют — разные кеш-ключи по cookie. Резко снижает hit rate.
  • Render-time персонализация выносится в client-side (useEffect, fetch) — серверный HTML общий, кешируется агрессивно, личные данные подгружаются после.

Третий вариант — самый хитрый и эффективный.

Итого

Кеш — это слои. Не пытайтесь решить всё одним Redis. Браузерный кеш + CDN снимают 80% нагрузки бесплатно. Redis под прикладной кеш — для динамики. In-memory — для совсем горячего. И всегда продумывайте инвалидацию ДО внедрения, не после.

Частые вопросы

Redis или Memcached?

Redis — стандарт 2020-х. Поддерживает структуры данных (hashes, lists, sorted sets), pub/sub, persistence, кластеризацию. Memcached — проще и чуть быстрее для plain key-value, но почти ничего больше не умеет. Для нового проекта берите Redis, через год понадобятся очереди, locks или pub/sub — всё в нём же.

Сколько RAM нужно Redis для среднего сайта?

Для типичного e-commerce с 50 тыс. товаров и 5 тыс. активных сессий — 2-4 ГБ. Для SaaS с миллионами объектов — 16-32 ГБ. Если упираетесь — настройте maxmemory-policy allkeys-lru, Redis сам начнёт выкидывать редко используемое. Когда eviction rate растёт — пора расширяться.

Cloudflare или Yandex CDN для российской аудитории?

Yandex CDN — для аудитории в РФ узлы по всей стране, никаких санкций, оплата рублями. Cloudflare — глобально лучший, но в РФ есть периодические замедления, оплата сложна. Если 90% пользователей в РФ — Yandex или Selectel CDN. Если глобальная аудитория — Cloudflare как основной, для России можно дополнительно подключать.

Как кешировать API-ответы для авторизованного пользователя?

Только в Redis с ключом, включающим userId: user:42:profile. Сбрасывать по событиям изменения (user.updated → DEL user:42:*). Никогда не кладите личные данные на CDN или в shared HTTP-кеш — отдадутся другому пользователю. Можно кешировать в браузере (Cache-Control: private), но не в CDN (s-maxage запрещен).

ISR в Next.js или свой кеш в Redis?

Для статически кешируемых страниц (блог, лендинги, каталог) — ISR проще, встроено, само инвалидируется по revalidateTag. Для API-данных, агрегаций, ответов внешним системам — Redis. Часто и то, и другое в одном проекте: ISR на страницах, Redis в API-роутах.

Что делать с кешированием при выкатке нового релиза?

Bundle статики — самоинвалидируется по хешу в имени файла. HTML — по версии build, или принудительный сброс CDN. Redis-кеш — обычно не сбрасывается (TTL разрулит сам). Если меняется формат данных в кеше (например, добавили поле) — версионируйте ключи: v2:product:42 вместо product:42. Старый ключ протухнет по TTL.

Почему hit rate в моём кеше всего 30%?

Скорее всего: 1) ключи слишком уникальные (включают timestamp или userId, но кешируете публичные данные), 2) TTL слишком короткий, 3) трафик распылён по миллиону уникальных URL (например, поисковые запросы). Разберите топ-100 запросов — что реально стоит кешировать с длинным TTL, а что нет смысла. Часто проще не кешировать вовсе и оптимизировать БД.