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

«Что такое кеширование в контексте веб-приложений?» — вопрос из категории Архитектура, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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