Кэш: где и как
Интуиция: холодильник на кухне
Представь, что каждый раз за молоком ты ходишь в магазин. Долго. Поэтому молоко лежит в холодильнике, а магазин нужен, только когда оно закончилось. Кэш — это такой холодильник: быстрое хранилище рядом с тем, кто читает данные. Медленный источник, то есть база, нужен только при промахе.
Две проблемы тоже как с холодильником: молоко может прокиснуть (данные устарели), и холодильник маленький (надо решать, что выкинуть). Весь урок про это.
Уровни кэша: от пользователя к базе
Кэш бывает на каждом шаге пути запроса. Чем ближе он к пользователю, тем быстрее ответ и тем сложнее этот кэш сбросить.
| Уровень | Что хранит | Задержка | Как инвалидировать |
|---|---|---|---|
| Браузер | статика, ответы с Cache-Control |
~0 мс | почти никак, только ждать TTL или менять URL (app.v42.js) |
| CDN | картинки, JS, иногда публичные API-ответы | 10–30 мс | purge через API провайдера |
| Кэш в памяти процесса | горячие мелкие данные, конфиги | наносекунды | у каждого инстанса своя копия, они расходятся |
| Кэш приложения (Redis/Memcached) | профили, ленты, сессии | ~1 мс | DEL key, общий для всех инстансов |
| Кэш базы | buffer pool, страницы в RAM | внутри БД | управляет сама БД |
На собесе пройдись по уровням сверху вниз одной фразой: «Статику отдаю через CDN с долгим TTL и версионированными URL, горячие объекты держу в Redis, для совсем горячих конфигов подойдёт локальный in-memory кэш с коротким TTL». Это сразу показывает зрелость.
Cache-aside: стратегия по умолчанию
Самая частая схема. Приложение само управляет кэшем:
- Пришёл запрос, ищем ключ в Redis.
- Hit: сразу отдаём ответ.
- Miss: читаем из PostgreSQL, кладём в Redis с TTL, отдаём ответ.
- При записи обновляем базу и удаляем ключ из кэша. Следующее чтение подтянет свежие данные.
Почему ключ при записи удаляют, а не обновляют? Два параллельных апдейта могут записать в кэш значения в обратном порядке. Удаление безопаснее.
Посчитаем, зачем это всё
Пусть 10 000 чтений в секунду. Redis отвечает за 1 мс, БД за 10 мс, hit rate 90%.
| Метрика | Расчёт | Итог |
|---|---|---|
| Запросов в БД | 10 000 × 0.1 | 1 000 RPS вместо 10 000 |
| Средняя задержка | 0.9 × 1 + 0.1 × (1 + 10) | ≈ 2 мс вместо 10 |
| При hit rate 99% | 10 000 × 0.01 | 100 RPS в БД |
Каждая «девятка» hit rate снимает с базы ещё порядок нагрузки. Скажи это вслух на собесе, это сильный аргумент.
Четыре стратегии: сравнение
| Стратегия | Как работает | Плюсы | Минусы |
|---|---|---|---|
| Cache-aside | приложение читает кэш, при промахе идёт в БД и кладёт результат в кэш | просто; падение кэша не роняет систему; в кэше только нужные данные | первый запрос медленный; логика кэша размазана по коду; возможна устаревшая выдача |
| Read-through | при промахе кэш сам ходит в БД (через библиотеку или провайдер) | код приложения чище | кэш обязан уметь читать БД; первый промах всё равно медленный |
| Write-through | запись идёт в кэш, кэш синхронно пишет в БД | кэш всегда свежий | запись медленнее (две операции); в кэш попадают данные, которые никто не читает |
| Write-back (write-behind) | запись только в кэш, в БД уходит пачками асинхронно | очень быстрая запись; меньше нагрузки на БД | риск потерять данные, если кэш упал до сброса; сложнее консистентность |
Правило выбора: по умолчанию бери cache-aside. Write-through нужен, когда важна свежесть при чтении сразу после записи. Write-back подходит для счётчиков, лайков и метрик, где потеря пары секунд данных некритична.
TTL: срок годности
TTL (time to live) задаёт, сколько живёт ключ. Это страховка: даже если ты забыл инвалидировать ключ, через TTL он сам исчезнет.
- Короткий TTL (секунды): данные свежее, но промахов больше.
- Длинный TTL (часы): hit rate выше, но пользователь дольше видит старое.
- Добавляй джиттер:
TTL = 300 ± random(30). Иначе тысячи ключей истекут одновременно, и вся нагрузка разом упадёт на БД.
Вытеснение: что выкинуть из полного холодильника
Память конечна. Когда она кончается, срабатывает политика вытеснения. Самая популярная из них LRU (Least Recently Used): выкидываем то, к чему дольше всего не обращались.
При каждом чтении ключ переезжает в голову списка. Есть ещё LFU (выкидываем редко используемое). Она лучше, когда популярность стабильна. В Redis это настройка maxmemory-policy allkeys-lru.
Прогрев: холодный старт
После деплоя или рестарта Redis кэш пуст, hit rate равен 0%, и все 10 000 RPS бьют в базу. Прогрев означает, что кэш заранее наполняют горячими ключами: топ товаров, популярные профили, данные с прошлого дня. Ещё помогают постепенный ввод трафика и защита от stampede: когда горячий ключ истёк, в БД за ним идёт только один запрос (лок или single-flight), остальные ждут.
Как это звучит на собесе
«Нагрузка read-heavy, 100:1, поэтому ставлю Redis по схеме cache-aside. TTL 5 минут с джиттером, при записи делаю DEL ключа. Ожидаю hit rate около 95%, это снимет с БД в 20 раз нагрузки. Если Redis упадёт, система деградирует, но продолжит работать через БД. Для статики CDN с версионированными URL».
Частые ошибки
- Кэшировать без TTL. Устаревшие данные будут жить вечно.
- Не назвать цену. Кэш всегда даёт eventual consistency, это нужно проговорить.
- Забыть про холодный старт и stampede. Интервьюер почти наверняка спросит: «А что будет, если кэш упал?»
- Write-back для денег. Платежи нельзя терять, тут только надёжная запись в БД.
- Локальный кэш без оговорок. Объясни, что копии на разных инстансах расходятся, поэтому TTL должен быть коротким.
Ты теперь умеешь ответить на вопросы «где», «как», «сколько живёт» и «что выкинуть». Это и есть весь каркас ответа про кэш.