loader
СГБ - Сологуб
Заказать услуги

Материалы с меткой: кэширование

Вчера в 09:25 Читать ~ 4 минуты 6
0ответов
Example blog post alt

Когда сайт на PHP начинает тормозить, первым делом смотрю на то, что там генерируется на каждый запрос. Меню, хлебные крошки, выгрузки из базы, тяжёлые виджеты с кучей запросов — всё это часто можно просто сохранить в файл и отдавать из кэша, а не пересчитывать заново для каждого посетителя. Делюсь своим классом для файлового кэширования, который я использую в проектах, и объясню, чем он отличается от простой версии из интернета.

Зачем вообще кэшировать в файлы

Идея простая: один раз сформировать контент, сохранить во временный файл и при следующих обращениях отдавать готовый результат, пока кэш не устарел или не сброшен вручную. Для блогов, каталогов и корпоративных сайтов без сложной реалтайм-логики это даёт ощутимый прирост скорости без подключения Redis или memcached. Файловый кэш не требует отдельного сервиса — просто папка на диске, которую можно почистить в любой момент.

Что я изменил в базовой реализации

Стандартный класс из туториалов работает, но слишком прямолинейно. Я доработал его под реальные задачи:

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

Сделал параметр hard — жёсткое обновление. Иногда нужно принудительно пересобрать кэш прямо сейчас, даже если срок ещё не вышел, например, после импорта товаров. Раньше для этого приходилось лезть на сервер и удалять файлы руками.

Добавил корректное сохранение с атомарной записью, чтобы файл не побился в момент одновременных запросов. Это критично, когда два посетителя одновременно дёргают страницу и оба пытаются перезаписать один и тот же кэш.

Встроил автоматическую вложенность папок, чтобы при большом количестве ключей не создавать одну гигантскую директорию с тысячами файлов — это тоже замедляет файловую систему.

Сам класс

 <?php
/** * Класс для кэширования фрагментов контента или страниц целиком. * Сохраняет сгенерированный вывод в файлы с учётом времени жизни. */
class FileCache
{ /** @var string Директория для хранения файлов кэша */ private static string $dir = __DIR__ . '/cache'; /** @var int Время жизни кэша по умолчанию, в секундах */ private static int $ttl = 3600; /** @var array Стек активных буферов (для поддержки вложенных вызовов) */ private static array $stack = []; /** * Настроить директорию кэша. */ public static function setDir(string $dir): void { self::$dir = rtrim($dir, '/'); } /** * Установить время жизни кэша по умолчанию, в секундах. */ public static function setTtl(int $seconds): void { self::$ttl = max(1, $seconds); } /** * Начать кэширование фрагмента. * * @param string $key Уникальный ключ фрагмента. * @param int|null $ttl Время жизни кэша в секундах, либо null для значения по умолчанию. * @param bool $hard Принудительно пересобрать кэш, игнорируя актуальный файл. * @return bool true, если кэш отсутствует и нужно вывести контент; false, если контент отдан из файла. */ public static function begin(string $key, ?int $ttl = null, bool $hard = false): bool { $ttl = $ttl ?? self::$ttl; // Превращаем ключ в безопасное имя файла $hash = md5($key); $file = self::$dir . '/' . substr($hash, 0, 2) . '/' . $hash . '.cache'; // Если жёсткое обновление не требуется и файл ещё актуален — отдаём из кэша if (!$hard && is_file($file) && (time() - filemtime($file)) < $ttl) { echo file_get_contents($file); self::$stack[] = null; // держим стек сбалансированным return false; } // Открываем буфер и запускаем "тяжёлый" вывод ob_start(); self::$stack[] = [ 'file' => $file, 'key' => $key, ]; return true; } /** * Завершить кэширование и сохранить вывод в файл. */ public static function end(): void { $item = array_pop(self::$stack); if ($item === null) { // Кэш был отдан из файла, сохранять нечего return; } $content = ob_get_clean(); // Готовим директорию с подпапкой $subdir = dirname($item['file']); if (!is_dir($subdir)) { mkdir($subdir, 0755, true); } // Атомарная запись во временный файл с последующим переименованием $tmp = $item['file'] . '.' . uniqid('tmp', true); file_put_contents($tmp, $content, LOCK_EX); rename($tmp, $item['file']); echo $content; } /** * Удалить кэш по конкретному ключу. */ public static function forget(string $key): void { $hash = md5($key); $file = self::$dir . '/' . substr($hash, 0, 2) . '/' . $hash . '.cache'; if (is_file($file)) { unlink($file); } } /** * Полностью очистить весь кэш, опционально только истёкший. */ public static function clear(bool $onlyExpired = false): void { if (!is_dir(self::$dir)) { return; } $items = new RecursiveIteratorIterator( new RecursiveDirectoryIterator(self::$dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST ); foreach ($items as $item) { if ($item->isDir()) { continue; } $path = $item->getPathname(); if ($onlyExpired && (time() - $item->getMTime()) < self::$ttl) { continue; } if (substr($path, -6) === '.cache') { unlink($path); } } } /** * Очистить кэш, истёкший по времени жизни. */ public static function clearExpired(): void { self::clear(true); }
}

Как этим пользоваться

Подключаем класс и сразу кэшируем нужный фрагмент:

 require_once __DIR__ . '/FileCache.php';
FileCache::setDir(__DIR__ . '/cache');
FileCache::setTtl(3600); // час по умолчанию
if (FileCache::begin('main_menu')) { // Тяжёлый вывод: например, меню собранное из базы ?> <nav>...большое меню...</nav> <?php FileCache::end();
}

Теперь меню сгенерируется один раз, сохранится, и в течение часа будет отдаваться из файла. Через час класс сам пересоберёт кэш при первом обращении.

Принудительно сбросить конкретный фрагмент после обновления данных:

 FileCache::forget('main_menu');

Или жёстко пересобрать прямо в момент вывода, не трогая остальной кэш:

 if (FileCache::begin('main_menu', hard: true)) { ?> <nav>...свежее меню...</nav> <?php FileCache::end();
}

Для полной очистки, например, после импорта товаров, хватает одного вызова:

 FileCache::clear();

  А если нужно убрать только устаревшие файлы без затрагивания актуальных — clearExpired(). Это удобно ставить в крон раз в сутки, чтобы папка кэша не разрасталась бесконечно.  

Что важно учесть на практике

Папку с кэшем нельзя складывать в публичную директорию. Если файлы кэша окажутся в public_html без защиты, кто угодно сможет открыть их напрямую. Я всегда выношу кэш за пределы веб-рута или закрываю доступ через .htaccess.

Ключи лучше делать осмысленными и уникальными. main_menu, catalog_sidebar, footer — понятно, что внутри. Добавляйте в ключ язык или ID товара, если у проекта есть мультиязычность или много динамических сущностей, иначе получите один и тот же кэш для разных данных.

Время жизни подбирается под задачу. Меню можно кэшировать сутками, а блок с актуальными ценами — на минуты. Поэтому в моём классе ttl задаётся отдельно для каждого фрагмента, а не только глобально.

Для проектов на Битриксе и других CMS нужно аккуратно смотреть, в каком месте подключать такой кэш, чтобы не сломать встроенную систему кэширования самой платформы. Здесь лучше кэшировать точечно отдельные блоки, а не пытаться завернуть в файл всю страницу целиком.

Когда это реально оправдано

Файловый кэш — это не панацея под любую задачу. Он отлично работает для слабо меняющегося контента: меню, подвалов, списков категорий, «горячих» выгрузок с пагинацией. А вот корзину, личный кабинет, актуальные остатки и всё, что зависит от текущего пользователя, кэшировать так нельзя — получите чужие данные в чужой сессии.

Если сайт крупный и нагрузки большие, рано или поздно упираетесь в то, что файловая система на каждом запросе медленнее любых in-memory хранилищ. Тогда стоит смотреть в сторону Redis. Но для большинства проектов на PHP файловый кэш — простой и надёжный первый шаг, который не требует ничего, кроме включённого PHP.

x
Тема пуша
Сообщение пуша
Наверх
Отправить заявку
Нажимая на кнопку, вы даете согласие на обработку своих персональных данных