«Сайт тормозит — поставим 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.»
При изменении данных нужно сбросить кеш. Варианты:
- TTL: ничего не сбрасываем, ждём истечения. Простой, но устаревшие данные на TTL секунд.
- Явная инвалидация: после
UPDATEделаемDELнужного ключа. Сложно с зависимостями («поменялась цена — а сколько кешей затронуто?»). - Tag-based (next.js, упрощённо): помечаем кеш тегами, инвалидируем по тегу.
// Next.js fetch
const data = await fetch(url, { next: { tags: ["products"], revalidate: 3600 } });
// при мутации
import { revalidateTag } from "next/cache";
revalidateTag("products");
- 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, а что нет смысла. Часто проще не кешировать вовсе и оптимизировать БД.