Что кэшировать и где
Кэш — это осознанный размен свежести данных на задержку и стоимость. Поэтому первый вопрос не «Redis или Memcached», а «какую устаревшесть допускает продукт для этих данных»: секунду, минуту, сутки. Ответ на него определяет и время жизни записей, и способ инвалидации, и вообще уместность кэша.
Кандидат на кэширование — то, что часто читают, редко меняют и дорого вычисляют. Определяющая метрика — доля попаданий, и она нелинейно влияет на выигрыш: девяносто процентов попаданий снижают нагрузку на источник в десять раз, а пятьдесят — всего вдвое, и добавленная сложность может не окупиться. Считать долю попаданий полезно по типам ключей, а не в среднем: одна плохо кэшируемая группа запросов легко прячется за хорошей общей цифрой.
Кэширование почти всегда многоуровневое: браузер, сеть доставки контента на границе, обратный прокси перед приложением, распределённый кэш вроде Redis, локальный кэш в процессе, кэш страниц и планов в самой базе. Чем ближе к пользователю обслужен запрос, тем он дешевле, поэтому слои выстраивают так, чтобы до базы доходило как можно меньше.
Локальный кэш в памяти процесса даёт наносекунды и переживает недоступность распределённого кэша, но у каждого инстанса своя копия, и согласованно инвалидировать её без шины событий невозможно. Поэтому в него кладут маленькие и редко меняющиеся справочники, а не пользовательские данные.
И принципиальное требование: сервис не должен ломаться при холодном или недоступном кэше. Если система выдерживает нагрузку только при полном кэше, то первый же перезапуск Redis становится инцидентом. Значит, нужны прогрев, ограничение конкурентных обращений к базе и заранее продуманная деградация ответа.

Стратегии чтения и записи
Стратегия по умолчанию — кэш сбоку: приложение само читает кэш, при промахе идёт в базу и кладёт значение обратно. Источником истины остаётся база, потеря кэша не теряет данные, а кэшировать можно что угодно, включая результаты сложных вычислений. Плата — явная логика в коде и возможность гонки между читателем и писателем.
Сквозное чтение переносит эту логику внутрь кэша: при промахе он сам обращается к источнику. Код приложения становится проще, но нужен провайдер с такой поддержкой, а поведение при недоступности источника определяется уже не вами.
Сквозная запись обновляет кэш и базу синхронно: данные в кэше всегда свежие, но каждая запись становится дороже, а кэш заполняется значениями, которые могут никогда не понадобиться. Отложенная запись — противоположный компромисс: пишем в кэш, а в базу сбрасываем асинхронно пачками. Это очень быстро и хорошо подходит счётчикам и метрикам, но при падении узла теряются последние секунды данных, поэтому для денег и заказов такой режим неприемлем.
Запись мимо кэша означает, что новые данные попадают только в базу, а в кэш — лишь при первом чтении. Приём полезен при интенсивной записи с редким чтением свежих записей: он не забивает память тем, что никто не запросит.
Выбор диктует профиль нагрузки: преобладание чтения и допустимая устаревшесть — кэш сбоку со временем жизни; требование свежести — инвалидация по событию изменения; преобладание записи — мимо кэша или отложенная запись. Универсального ответа нет, и на собеседовании ценится именно связка «профиль нагрузки → стратегия → цена».
// Чтение: кэш сбоку. Время жизни с разбросом, чтобы ключи не истекали синхронно.
Obj o = cache.get(id);
if (o == null) {
o = db.find(id);
cache.set(id, o, ttl + random(0, ttl / 4));
}
return o;
// Запись: удалять ключ, а не перезаписывать —
// иначе параллельный читатель может положить поверх устаревшее значение.
db.update(id, patch);
cache.delete(id);
Инвалидация и гонка с записью
Время жизни записи — самый дешёвый и самый надёжный механизм: он не требует координации и ограничивает ущерб от любой пропущенной инвалидации. Плата очевидна — окно, в течение которого пользователь видит старое. Значение выбирают из допустимой устаревшести, а не из привычки: для каталога это минуты, для курса валют секунды, для справочника стран сутки.
Явное удаление ключа после записи даёт свежесть, но в распределённой системе оно не атомарно с изменением в базе. Если приложение упало между коммитом и обращением к кэшу или кэш в этот момент был недоступен, значение залипнет до истечения времени жизни. Поэтому время жизни оставляют всегда, даже при явной инвалидации, — как страховку от пропущенных событий.
Классическая гонка выглядит так: читатель промахнулся и прочитал значение из базы; в этот момент писатель обновил строку и удалил ключ из кэша; после этого читатель кладёт в кэш прочитанное ранее устаревшее значение — и оно живёт до конца времени жизни. Смягчают это коротким временем жизни, включением версии или отметки времени изменения в сам ключ, записью в кэш только при совпадении версии, либо инвалидацией по журналу изменений базы, которая по определению происходит после коммита.
Отсюда практическое правило: после записи ключ удаляют, а не перезаписывают. Удаление идемпотентно, не зависит от порядка и не может записать устаревшее значение; следующий читатель просто сходит в базу за свежим. Перезапись же требует уверенности в порядке операций, которой в распределённой системе нет.
Инвалидация через шину событий или через захват изменений базы — единственный способ согласованно чистить кэши нескольких сервисов и локальные кэши всех инстансов. Цена — инфраструктура и задержка доставки, а также обязанность обрабатывать события идемпотентно и переживать их повторы.
Stampede, лавина и пустые ключи
Давка возникает, когда популярный ключ истекает и сотни параллельных запросов одновременно уходят в базу за одним и тем же значением. Особенно опасно, когда тысячи горячих ключей записаны с одинаковым временем жизни: они истекают синхронно, и база получает согласованный залп.
Минимальная обязательная мера — разброс времени жизни: случайная добавка разводит истечение ключей во времени и стоит одну строку кода. Дальше идёт пересчёт в один поток: только первый запрос идёт в базу, остальные ждут результат или получают предыдущее значение. Третий приём — отдавать устаревшее значение сразу и обновлять его в фоне, что почти всегда лучше для пользователя, чем ждать пересчёта.
Отдельная разновидность — запросы к несуществующим ключам: они всегда промахиваются мимо кэша и бьют прямо в базу, что делает их удобным вектором для намеренной перегрузки. Лечится кэшированием отрицательного результата с коротким временем жизни, а при большом пространстве ключей — вероятностным фильтром, который дёшево отсекает заведомо отсутствующие значения.
Самый тяжёлый сценарий — лавина, когда кэш целиком становится недоступен или перезапускается. Весь трафик мгновенно уходит в базу, которая рассчитана на десятую его часть, и падает уже она. Защита строится не в кэше, а вокруг него: ограничение числа одновременных обращений к базе, предохранитель, урезанный ответ и постепенный прогрев вместо мгновенного открытия полного трафика.
Вывод, который стоит произнести на собеседовании: как только кэш начинает нести основную нагрузку, он перестаёт быть оптимизацией и становится критической зависимостью — со своим резервированием, наблюдением и планом на случай отказа.
Вытеснение и горячие ключи
Память кэша конечна, поэтому нужна политика вытеснения. Вытеснение давно не использованных — разумное значение по умолчанию и хорошо ловит смену интереса. Вытеснение редко используемых лучше при стабильном распределении популярности, но хуже реагирует на изменения. В Redis политику выбирают ещё и по области действия: только ключи со временем жизни или любые, а вариант с отказом от вытеснения означает, что при заполнении памяти записи начнут завершаться ошибкой — иногда это осознанный выбор, но чаще неприятный сюрприз.
Размер кэша оценивают не по всему объёму данных, а по рабочему множеству: обращения распределены крайне неравномерно, и небольшая горячая доля даёт основную часть попаданий. Именно поэтому кэш вообще работает, и именно поэтому вопрос «сколько стоит держать в памяти горячие данные за последние сутки» продуктивнее вопроса «сколько у нас всего данных».
Горячий ключ — ситуация, когда один популярный ключ собирает нагрузку, которую физически обслуживает один узел кэша. Симптом однозначен: перекос загрузки узлов при равномерном на вид распределении. Лечат локальной копией этого ключа в памяти инстансов перед распределённым кэшем, размножением ключа с суффиксом и случайным выбором копии при чтении, а для счётчиков — шардированием самого счётчика с суммированием при чтении.
Зеркальная проблема — слишком большие значения. Один объект в несколько мегабайт занимает однопоточный сервер кэша на всё время операции, забивает сеть и делает задержку неравномерной для всех остальных клиентов. Такие значения дробят на части, сжимают или не кэшируют вовсе.
CDN и ключ кэша
Сеть доставки контента — это кэш на узлах, физически близких к пользователю. Она уменьшает задержку, снимает нагрузку с источника, гасит всплески и заодно завершает TLS ближе к клиенту. Для глобальной аудитории это не оптимизация, а необходимость: межконтинентальный обмен нельзя ускорить кодом.
Для статики стандартом стал приём с версией в имени файла и неизменяемым ответом: файл отдаётся с максимальным сроком жизни и пометкой, что он никогда не меняется, а при новой сборке меняется само имя. Инвалидация при этом не нужна вовсе — она заменена сменой ключа, и это самый надёжный вид инвалидации из существующих.
Для динамики работают заголовками: срок жизни отдельно для общих кэшей, разрешение отдавать устаревшее во время обновления, условные запросы по идентификатору версии ответа. Здесь же — главный риск: приватный ответ, закэшированный как общий, утекает другим пользователям. Персональные данные обязаны помечаться как приватные, а зависимость ответа от заголовков — объявляться явно, иначе один пользователь увидит кабинет другого.
Ключ кэша должен включать всё, от чего зависит ответ, — язык, версию API, кодировку сжатия, регион — и не включать ничего лишнего. Случайные метки рекламных кампаний в адресе превращают один популярный ресурс в тысячи разных ключей и обнуляют долю попаданий, поэтому ключ нормализуют, отбрасывая незначимые параметры.
Для приватного, но тяжёлого контента используют подписанные ссылки с ограниченным сроком: файл отдаётся с границы сети, но только тому, кто получил подпись от приложения. А чтобы промахи всех узлов не били в источник одновременно, между ними ставят промежуточный слой кэша, который собирает промахи и ходит к источнику один раз.
Как отвечать на собеседовании
Начинайте не с технологии, а с вопроса о допустимой устаревшести и о профиле чтения и записи. Фраза «прежде чем выбирать кэш, я спрошу, насколько свежими обязаны быть эти данные» сразу задаёт правильный тон и показывает, что вы понимаете природу компромисса.
Разложите ответ по слоям и назовите стратегию для каждого: граница сети для статики с версионированными именами, распределённый кэш для данных по схеме «кэш сбоку», локальный кэш только для маленьких справочников. Обязательно упомяните долю попаданий как метрику, по которой судят об успехе.
Проговорите проблемные места, не дожидаясь наводящих вопросов: гонка читателя с писателем и правило удалять, а не перезаписывать; давка при синхронном истечении и защита разбросом времени жизни, пересчётом в один поток и выдачей устаревшего; горячий ключ и лавина при падении кэша. Это те три сюжета, которые интервьюер обычно и хочет услышать.
Заканчивайте честной ценой: кэш добавляет ещё одно место, где данные могут быть неверными, ещё одну зависимость, которая может упасть, и ещё один источник трудновоспроизводимых ошибок. Поэтому кэшируют не всё подряд, а то, где выигрыш измерим, и всегда закладывают поведение системы при холодном и при недоступном кэше.