Миф: кеш решит проблему с производительностью
Сайт тормозит, API отвечает за секунды вместо миллисекунд — и первое, что приходит в голову: «добавим Redis, закешируем самые тяжёлые запросы, и всё полетит». Иногда это действительно работает и снимает проблему за час. Но чаще кеш просто прячет медленный запрос под слоем, который сам по себе не бесплатен: рано или поздно наступает промах кеша, перезапуск или инвалидация — и вы упираетесь в ту же самую медленную операцию, только теперь ещё и с новым классом багов вокруг неё.
Содержание
Где кеш действительно решает проблему
У кеширования есть чёткая область, в которой оно работает почти без компромиссов: повторяющиеся дорогие операции с одинаковым результатом. Если тысяча пользователей за минуту запрашивают один и тот же список категорий товаров, который меняется раз в час, — совершенно не нужно тысячу раз ходить в базу и пересчитывать один и тот же JOIN. Достаточно посчитать один раз, положить результат в Redis с TTL, и следующие 999 запросов отдать из памяти за доли миллисекунды.
Признаки задачи, которая создана для кеша:
- результат запроса детерминирован для одного и того же набора входных параметров;
- данные меняются заметно реже, чем запрашиваются (соотношение чтение/запись сильно в пользу чтения);
- вычисление или запрос дорогое — сложный агрегат, JOIN на нескольких таблицах, внешний API с лимитами, тяжёлый рендеринг шаблона;
- допустима небольшая задержка актуальности — читатель не заметит, если список категорий обновится не мгновенно, а через 30–60 секунд.
Классический пример — курсы валют, обновляемые раз в 5 минут, конфигурация фичефлагов, результаты подсчёта рейтинга, HTML-фрагменты хедера и футера сайта, справочники (страны, часовые пояса, тарифы). Здесь кеш не «затыкает» проблему — он именно решает её, потому что пересчитывать одно и то же на каждый запрос действительно избыточно.
Но дальше начинается миф: если кеш так хорошо работает для этого класса задач, значит, он решит и любую другую проблему производительности. Это не так, и дальше — почему именно.
Кеш маскирует симптом, а не лечит причину
Самая частая ошибка — тащить кеш поверх медленного запроса вместо того, чтобы разобраться, почему он медленный. Представьте: запрос к PostgreSQL без индекса на поле user_id выполняет full table scan по таблице на 5 миллионов строк и занимает 800 мс. Вы оборачиваете его в кеш на 10 минут — среднее время ответа падает почти до нуля, метрики выглядят отлично. Проблема решена? Нет: сам запрос как выполнялся 800 мс, так и выполняется — просто теперь это происходит не при каждом обращении, а раз в 10 минут при промахе кеша (cache miss).
Разница принципиальная: без индекса это будет 800 мс *каждый раз*, когда кеш реально понадобится — а понадобится он ровно в тот момент, когда нагрузка выше обычной (иначе зачем кешировать) или после рестарта. Вы не убрали медленную операцию, вы отложили её на самый неудобный момент.
Проверить первопричину — 5 минут работы:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE user_id = 12345 ORDER BY created_at DESC LIMIT 20;
Если в плане видно Seq Scan on orders вместо Index Scan, дело не в том, что базе «нужен кеш», а в том, что не хватает индекса:
CREATE INDEX CONCURRENTLY idx_orders_user_created
ON orders (user_id, created_at DESC);
После индекса тот же запрос может выполняться за 2–5 мс вместо 800 — и тогда вопрос «нужен ли кеш» вообще снимается или переходит в разряд «опциональная оптимизация», а не «единственный способ не упасть». Правило простое: кеш имеет смысл добавлять *после* того, как операция уже настолько быстрая, насколько это возможно естественным путём — а не вместо этого шага.
То же самое с приложением: если N+1-запрос делает 200 обращений к базе на один HTTP-запрос, обернуть весь ответ в кеш — не решение архитектурной проблемы, а откладывание её. Как только кеш инвалидируется (а инвалидируется он неизбежно), эти 200 запросов снова обрушатся на базу разом.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверИнвалидация кеша — новый класс сложных проблем
Есть известная шутка про две самые сложные вещи в программировании: инвалидация кеша и придумывание имён переменных. Она не случайна. Как только вы вводите кеш, вы автоматически вводите вопрос: «а как я узнаю, что закешированные данные устарели?» — и простого универсального ответа на него нет.
Три типовых сценария и где они ломаются:
TTL (время жизни). Проще всего поставить EXPIRE key 300 и не думать об инвалидации вручную. Но это компромисс: либо TTL слишком короткий — и вы теряете большую часть выгоды от кеша (частые промахи), либо слишком длинный — и пользователи видят устаревшие данные (цена товара, статус заказа, баланс) минутами. Для контента, где расхождение в секунды не критично, TTL отлично работает. Для баланса счёта или статуса оплаты — это уже баг, который будет разбирать поддержка.
Инвалидация по событию. Более точный, но более хрупкий подход: при изменении данных явно удалять или обновлять соответствующий ключ кеша.
def update_user_profile(user_id, data):
db.update("users", user_id, data)
redis.delete(f"user:profile:{user_id}")
Проблема в том, что таких мест обновления обычно не одно — профиль пользователя меняется из веб-формы, из админки, из фонового джоба синхронизации, из вебхука платёжной системы. Пропустили один путь обновления — и в кеше зависла стухшая версия, которая может прожить там до истечения TTL (если он вообще есть) или бессрочно.
Рассинхронизация кеша и источника истины. Даже при аккуратной инвалидации возможна гонка: два параллельных запроса читают из базы, один обновляет запись, второй кладёт в кеш уже устаревшее значение, которое успел прочитать до обновления. При высокой конкурентности это не гипотетическая, а регулярная ситуация. Лечится версионированием ключей, паттерном write-through (запись одновременно в базу и в кеш в рамках одной операции) или явной блокировкой при обновлении — но это уже дополнительная инженерная работа, а не «поставили Redis и забыли».
Итог раздела простой: кеш переносит часть сложности из «медленно, но правильно» в «быстро, но нужно доказать, что не показываем устаревшее». Для одних данных это выгодный обмен, для других — нет.
Холодный старт и эффект стада
Отдельная категория проблем — то, что происходит с кешем не в установившемся режиме, а в моменты разрыва: перезапуск Redis, деплой новой версии приложения, истечение TTL у популярного ключа одновременно у тысяч пользователей.
Если кеш перезапускается (обновление, авария, миграция на новый инстанс) — он стартует пустым. Все запросы, которые раньше обслуживались из памяти за доли миллисекунды, одновременно идут в базу, которая рассчитана на нагрузку «долю запросов, промахнувшихся мимо кеша», а не «100% трафика разом». Это классический сценарий, из-за которого база, прекрасно работавшая с кешем месяцами, вдруг ложится в первые секунды после рестарта кеш-сервера. Подробный разбор именно этой ситуации и вариантов защиты (прогрев кеша перед переключением трафика, staggered TTL, jitter) — в статье про эффект стада при прогреве кеша.
Похожая механика — thundering herd на уровне одного ключа: если очень популярный ключ (например, главная страница интернет-магазина) имеет TTL 60 секунд, то в момент истечения этой секунды тысячи параллельных запросов одновременно обнаруживают промах и одновременно бросаются пересчитывать одно и то же значение, создавая кратковременный, но острый всплеск нагрузки на источник данных. Решается блокировкой пересчёта (только один запрос идёт в базу, остальные ждут или получают чуть устаревшее значение) — паттерн часто называют cache stampede protection или single-flight.
Мораль: кеш не только не устраняет пиковую нагрузку на базу — при неудачной конфигурации он может её *концентрировать* во времени, превращая размазанную по часу нагрузку в резкий всплеск в момент инвалидации или рестарта.
Когда кеш почти бесполезен
Кеш эффективен ровно настолько, насколько высок процент попаданий (hit rate). Если данные не подходят под профиль «часто читаем, редко меняем, значение повторяется между запросами» — кеш либо не даёт выигрыша, либо даёт минимальный при заметной добавленной сложности.
Типичные случаи, где кеширование почти не помогает:
| Тип данных | Почему кеш не спасает |
|---|---|
| Персонализированная лента / рекомендации | Уникальны для каждого пользователя — ключ кеша практически не переиспользуется, hit rate близок к нулю |
| Данные, меняющиеся на каждую запись (счётчики просмотров в реальном времени) | TTL либо слишком короткий (нет выгоды), либо показывает устаревшее число |
| Поисковые запросы с произвольными параметрами | Комбинаций фильтров тысячи, повторение одного и того же запроса редкое |
| Данные с юридическими требованиями к актуальности (баланс, статус транзакции) | Устаревшее значение недопустимо, кеш добавляет риск без выгоды |
| Одноразовые тяжёлые вычисления (генерация отчёта по уникальному запросу пользователя) | Результат не переиспользуется — кешировать нечего |
Показательный пример — персонализированная главная страница с блоком «рекомендуем для вас» на основе истории покупок. Кешировать её целиком бессмысленно: для каждого пользователя блок свой, повторных попаданий почти не будет, а место в Redis и накладные расходы на сериализацию/десериализацию останутся. Правильный путь здесь — кешировать не всю страницу, а её переиспользуемые части (тот же хедер, футер, общий каталог), а персональный блок собирать отдельно, возможно, ускоряя не кешем, а денормализацией данных или предвычислением рекомендаций в фоне.
Что кеш не лечит в принципе
Кеш — это инструмент против медленного чтения повторяющихся данных. У него есть чёткие границы применимости, за которыми он не работает вообще, а не «работает похуже»:
- Проблемы записи. Кеш ускоряет чтение, но если узкое место — это медленная запись (много
INSERT/UPDATE, блокировки строк, конкуренция за один и тот же ряд), кеш перед базой ничем не поможет: писать всё равно нужно в источник истины. Здесь работают другие инструменты — батчинг записи, очереди сообщений, партиционирование таблиц, оптимизация схемы. - CPU-интенсивные уникальные вычисления. Если каждый запрос требует своего, не повторяющегося вычисления (например, генерация персонального PDF-отчёта с разными параметрами), кешировать нечего — результат не будет переиспользован. Тут нужна оптимизация самого алгоритма, распараллеливание или вынос в фоновую очередь с уведомлением по готовности.
- Проблемы сетевой топологии. Если сайт из Москвы грузится медленно, потому что сервер физически стоит в другой части света и запрос идёт через несколько магистральных провайдеров с задержкой 150–200 мс на каждый round-trip, — никакой Redis это не изменит. Здесь помогает выбор сервера ближе к аудитории, CDN для статики, HTTP/2 или HTTP/3 для сокращения числа round-trip'ов, но не кеширование данных на бэкенде.
- Недостаток ресурсов сервера. Если проблема в том, что серверу физически не хватает CPU или RAM под текущую нагрузку, кеш снизит нагрузку на базу, но не решит нехватку ресурсов приложения — оно всё равно будет упираться в лимиты при росте трафика.
Иными словами: прежде чем ставить Redis, стоит честно определить, к какому из этих классов относится ваша проблема. Если это не «повторяющееся дорогое чтение» — кеш просто не тот инструмент.
Как правильно подходить к производительности
Разумный порядок действий — сначала диагностика, потом решение, а не наоборот.
- Профилирование до всего остального. Найдите, что именно медленно: конкретный SQL-запрос, конкретный внешний вызов, конкретная функция в коде. Инструменты —
EXPLAIN ANALYZEдля SQL, APM-системы (например, встроенные профилировщики фреймворков),strace/perfна уровне ОС, логирование времени выполнения по этапам запроса.
- Проверьте очевидное перед кешем. Индексы под фактические запросы (
pg_stat_user_tables,pg_stat_statementsв PostgreSQL покажут самые тяжёлые и частые запросы), N+1-проблемы в ORM, отсутствие пагинации там, где выгружаются тысячи строк, неоптимальные JOIN.
SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
- Оцените профиль данных. Задайте себе три вопроса: как часто эти данные читаются относительно того, как часто меняются? Насколько допустима задержка актуальности в секундах или минутах? Насколько дорого их пересчитывать каждый раз? Если ответы — «читаются часто, меняются редко, задержка допустима, пересчёт дорогой» — кеш действительно подойдёт.
- Кешируйте точечно, а не «всё подряд». Не оборачивайте кешем весь ответ API — кешируйте конкретный дорогой и переиспользуемый кусок: результат тяжёлого агрегата, а не персонализированный ответ целиком.
- Продумайте инвалидацию заранее, а не после первого инцидента. TTL для данных, где допустима задержка; явную инвалидацию по событию — где нужна точность; защиту от stampede — для популярных ключей; план прогрева — для перезапуска кеш-слоя.
- Мониторьте hit rate. Низкий процент попаданий (условно ниже 70–80% для сценария, где кеш вообще имеет смысл) — сигнал, что либо TTL подобран неверно, либо данные не подходят под кеширование в принципе.
redis-cli info stats | grep keyspace
# keyspace_hits:184203
# keyspace_misses:9871
Соотношение hits к misses здесь — грубый, но полезный индикатор: если промахов заметная доля, кеш либо неправильно настроен, либо решает не ту задачу.
Если после диагностики выяснилось, что дело в нехватке ресурсов сервера, а не в архитектуре запросов, дальше уже вопрос выбора конфигурации — сколько CPU и RAM реально нужно под нагрузку с учётом слоя кеширования и без него.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Кеш вообще не нужен, если сделать индексы и оптимизировать запросы?
Нужен, но для другого класса задач. Даже идеально оптимизированный запрос с индексом занимает какое-то ненулевое время и нагружает базу при каждом обращении. Если один и тот же результат запрашивается тысячи раз в минуту, кеш всё равно снизит нагрузку и ускорит ответ — но уже как оптимизация поверх здорового запроса, а не как костыль вместо индекса.
Redis или Memcached — что выбрать для кеша перед базой?
Зависит от требуемого функционала: Memcached проще и по чистой скорости на простых ключ-значение операциях бывает немного быстрее, Redis богаче по возможностям (структуры данных, persistence, pub/sub, Lua-скрипты). Разбор различий и сценариев — в статье Redis или Memcached: что выбрать для сервера.
Как понять, что запрос стоит кешировать, а не оптимизировать?
Если после EXPLAIN ANALYZE и добавления нужных индексов запрос всё ещё занимает заметное время (десятки-сотни миллисекунд) из-за объективной сложности агрегации, а результат при этом переиспользуется многими запросами подряд — это кандидат на кеш. Если запрос медленный из-за отсутствия индекса — сначала чините индекс.
Кеш может сделать хуже, чем было без него?
Да. Неудачно настроенный кеш добавляет точку отказа (что если Redis недоступен?), задержку из-за сериализации, риск показа устаревших данных и сложность отладки («это в базе не так или в кеше устарело?»). Плюс проблема эффекта стада при перезапуске может создать пик нагрузки хуже, чем был бы без кеша вообще.
С чего начать, если непонятно, где именно тормозит система?
С профилирования, а не с добавления инструментов. Логируйте время каждого этапа обработки запроса (база, внешние вызовы, рендеринг), смотрите pg_stat_statements или аналог для вашей СУБД, проверьте план выполнения самых частых и самых тяжёлых запросов через EXPLAIN. Разбор того, когда индекс ускоряет, а когда неожиданно замедляет запрос, — в статье как индекс ускоряет запрос и когда замедляет.
Если Redis уже используется — как проверить, что он реально помогает, а не просто добавляет сложность?
Смотрите hit rate (redis-cli info stats), долю памяти, занятую редко читаемыми ключами, и что произойдёт с базой при полном сбросе кеша (можно проверить на стейджинге). Если после сброса база спокойно держит нагрузку без деградации — кеш скорее приятный бонус, а не критическая опора; если база падает — вы обнаружили скрытую зависимость, которую стоило бы явно защитить от эффекта стада. Частые проблемы конкретно с Redis на сервере разобраны отдельно в статье Redis на сервере: частые ошибки и решения.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →