Что такое кеширование в контексте веб-приложений?

Ответ

Кеширование — это стратегия хранения копии данных или результатов вычислений во временном, быстром хранилище (кеше) для последующего быстрого доступа, что позволяет избежать повторного выполнения дорогостоящих операций (запросов к БД, сложных вычислений, внешних API-вызовов).

Основные уровни и типы кеширования в веб-приложении:

  1. Кеширование на стороне клиента (браузер):

    • HTTP-кеширование: Использование заголовков Cache-Control, ETag, Last-Modified для кеширования статических ресурсов (CSS, JS, изображения) и даже динамических ответов API.
    • LocalStorage / SessionStorage: Для хранения данных приложения на стороне клиента.
  2. Кеширование на стороне сервера (Server-side):

    • Кеш приложения (In-memory): Хранение данных в оперативной памяти процесса (например, PHP-массив, apcu_store()). Быстро, но не разделяется между процессами/серверами.
    • Распределённый кеш (Distributed Cache): Использование таких систем, как Redis или Memcached. Данные хранятся в отдельном сервисе, доступном всем экземплярам приложения. Это решает проблемы масштабирования и разделения кеша.
    • Кеширование базы данных: Встроенные механизмы СУБД (например, кеш запросов в MySQL).
    • Опкод-кеш (OPcache): Для скомпилированного PHP-байткода, ускоряет выполнение скриптов.
  3. Прокси-кеширование и CDN:

    • Varnish, Nginx cache: Кеширование целых HTML-страниц перед веб-сервером.
    • CDN (Cloudflare, AWS CloudFront): Доставка статического и динамического контента с географически близких серверов.

Пример реализации кеширования результатов тяжёлого запроса с использованием Redis в PHP:

function getTopProducts(int $categoryId, int $limit = 10): array {
    $redis = new Redis();
    $redis->connect('redis-host', 6379);

    $cacheKey = "top_products:cat_{$categoryId}:limit_{$limit}";
    $ttl = 300; // Время жизни кеша — 5 минут

    // Попытка получить данные из кеша
    $cachedData = $redis->get($cacheKey);
    if ($cachedData !== false) {
        return json_decode($cachedData, true); // Возвращаем из кеша
    }

    // Данных в кеше нет — выполняем дорогой запрос к БД
    $db = new PDO('mysql:host=localhost;dbname=shop', 'user', 'pass');
    $stmt = $db->prepare("SELECT * FROM products WHERE category_id = ? ORDER BY sales DESC LIMIT ?");
    $stmt->execute([$categoryId, $limit]);
    $products = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // Сохраняем результат в Redis перед возвратом
    $redis->setex($cacheKey, $ttl, json_encode($products));

    return $products;
}

Критические аспекты:

  • Инвалидация кеша: Определение стратегии обновления кеша при изменении данных (TTL, ручная очистка, cache tagging).
  • Сквозное кеширование (Cache-Aside / Lazy Loading): Паттерн, использованный в примере выше — приложение сначала проверяет кеш, и только при промахе загружает данные из источника.
  • Согласованность данных: Кеш может содержать устаревшие (stale) данные, что необходимо учитывать в бизнес-логике.

Ответ 18+ 🔞

Давай разберём, что за зверь такой — кеширование, чтобы не было потом, как в том анекдоте, где сервер спрашивает базу: «Ну чё, опять?», а она ему: «Да похуй, ищи в кеше».

Представь, что твоё веб-приложение — это я, когда жена просит сходить в гараж за банкой варенья. Если бы у меня был кеш, я бы эту банку уже держал на кухне, под рукой, и не пришлось бы каждый раз, блядь, обуваться и идти через весь двор. Вот и кеш — это такое быстрое хранилище под рукой, чтобы не ходить каждый раз в медленное (базу данных, внешний API или на тот свет за результатами сложных вычислений).

Так где же эту банку с вареньем можно припрятать? Уровни кеширования:

  1. Прямо в кармане (на клиенте). Браузер — он хитрая жопа, может многое сам запомнить. Картинки, скрипты — если правильно настроить заголовки (Cache-Control и прочую муть), он их не будет таскать с сервера каждый раз. А ещё можно в его LocalStorage какую-нибудь ерунду запихнуть — данные формы, настройки. Удобно, но доверия к нему, ебать, ноль, потому что пользователь в любой момент может всё почистить.

  2. В серверной, под столом (на стороне сервера). Тут вариантов — овердохуища.

    • В памяти процесса (In-memory). Самый быстрый способ — запихнуть данные в обычный массив PHP. Но есть проблема: если у тебя несколько рабочих процессов или серверов, у каждого будет своя, отдельная память. Один процесс записал, другой нихуя не видит. Как два мужика в одном гараже, но с разными ключами.
    • В отдельном хранилище (Redis/Memcached). Вот это уже серьёзно. Заводишь отдельный сервис — Redis. Он теперь как общий сейф в прихожей. Все процессы со всех серверов приходят и кладут/берут данные оттуда. Масштабируется на ура. Почти всегда это и есть правильный выбор для шустрого кеша.
    • Кеш самой базы данных. База данных — тоже не дура, она частые запросы сама пытается запомнить. Но надеяться на это — всё равно что надеяться, что жена сама догадается, где ты носки спрятал. Ненадёжно.
  3. На подступах к крепости (Прокси и CDN). Это когда ты ставишь перед своим основным сервером здоровенную бандуху типа Varnish или подключаешь CDN. Они могут кешировать целые готовые HTML-страницы и отдавать их, даже не беспокоя твоё основное приложение. Это, блядь, уровень «я тебе всю полку с вареньем сразу в комнату принесу».

А теперь смотри, как это выглядит в коде на PHP с Redis. Чистая практика, ёпта.

function getTopProducts(int $categoryId, int $limit = 10): array {
    $redis = new Redis();
    $redis->connect('redis-host', 6379);

    // Придумываем ключ. Важно, чтобы он был уникальным для этого запроса.
    $cacheKey = "top_products:cat_{$categoryId}:limit_{$limit}";
    $ttl = 300; // Жить этому кешу 5 минут. Потом — на свалку истории.

    // Пробуем найти банку в кеше (в сейфе).
    $cachedData = $redis->get($cacheKey);
    if ($cachedData !== false) {
        // Ура, нашли! Возвращаем, не ходя в гараж (в базу).
        return json_decode($cachedData, true);
    }

    // Вот тут пиздец. В кеше пусто. Придётся идти в самую дальнюю, холодную БД.
    $db = new PDO('mysql:host=localhost;dbname=shop', 'user', 'pass');
    $stmt = $db->prepare("SELECT * FROM products WHERE category_id = ? ORDER BY sales DESC LIMIT ?");
    $stmt->execute([$categoryId, $limit]);
    $products = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // Но мы не дураки! Прежде чем вернуть результат, запихнём его в Redis.
    // В следующий раз он уже будет там.
    $redis->setex($cacheKey, $ttl, json_encode($products));

    return $products;
}

На чём споткнуться можно, или подводные ебушки-воробушки:

  • Инвалидация. Это самое сложное. Вот данные в базе изменились (цену на товар поменяли), а в кеше лежит старая версия. Как сказать кешу, что он говно? Можно жить по TTL (как в примере — через 5 минут само протухнет). Можно вручную удалять ключ при изменении данных. В общем, без стратегии тут — пиши пропало.
  • Паттерн «Cache-Aside» (или Lazy Loading). Это ровно то, что мы сделали в коде выше. Приложение само управляет кешем: «Сначала посмотри там, нет — сходи туда, принеси — положи сюда». Классика, работает на ура.
  • Согласованность. Надо чётко понимать: кеш — это всегда возможно устаревшие данные. Если тебе нужна стопроцентная свежесть на миллисекунду (например, баланс счёта), то кешировать это — идти по охуенно тонкому льду. А если топ товаров — да похуй, пусть пять минут полежит старый список, никто не умрёт.

Вот и вся магия. Главное — не превратить приложение в мартышку, которая только и делает, что бегает между Redis и БД, забывая, что у неё вообще бизнес-логика есть.