Кэширование и PHP. Простая и эффективная система кеширования PHP


кэширование в PHP

В старые добрые времена, когда создание веб-сайтов представляло из себя такое простое занятие, как набор нескольких HTML-страниц, отправка веб-страниц в браузер была простой отправкой файла веб-сервером. Посетители сайта могли видеть эти небольшие, исключительно текстовые странички, почти мгновенно (если не считать пользователей медленных модемов). Как только страница была загружена, браузер кэширует ее где-нибудь на локальном компьютере, чтобы в случае повторного запроса страницы можно было взять его локальную версию из кэша, послав лишь короткий запрос, чтобы убедиться, что страница на сервере не была изменена. Запросы обрабатывались быстро и наиболее эффективно, и все были счастливы (кроме использующих модемы на 9600 бод).

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

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

2. Типичный веб-сервер, например, Apache, использует время модификации файла, чтобы правильно сообщить браузеру состояние кэша запрашиваемой странички. В динамических веб-сайтах сам PHP-скрипт может изменяться только изредка, в то время как отображаемый им контент, возможно располагающийся в базе данных, изменяется часто. Веб-сервер не может узнать об изменениях в базе данных, поэтому он не отправляет дату последней модификации. Если клиент (браузер) не получает никакого признака того, как долго данные являются корректными, он предполагает, что в следующий раз необходимо запросить страничку по новой. Веб-сервер всегда будет отвечать обновленной версией странички, независимо от того, изменились ли данные. Чтобы избежать этого недостатка большинство веб-разработчиков используют мета-теги или HTTP-заголовки, чтобы приказать браузеру никогда не использовать кэшированную версию странички. Однако это отрицает естественную способность веб-браузера кэшировать веб-страницы и обладает некоторыми существенными недостатками. Например, содержание динамической странички может изменяться раз в сутки, поэтому выгода, получаемая от наличия даже 24-часового кэширования странички браузером, очевидна.

Обычно для маленьких PHP-приложений вполне можно игнорировать существование этих проблем, однако с увеличением сложности и повышением трафика вашего сайта вы можете столкнуться с проблемами. Тем не менее, обе эти проблемы могут быть решены: первая - путем кэширования на стороне сервера, вторая - путем управления кэшированием на стороне клиента из вашего приложения. Подход, который вы будете использовать, будет зависеть от ваших конкретных задач, но в этой статье мы покажем, как можно решить обе проблемы, используя PHP и некоторые классы библиотеки PEAR.

как я предотвращаю кэширование страницы браузерами?

Прежде чем мы рассмотрим методы клиентского и серверного кэширования, в первую очередь мы должны понять, как вообще предотвратить кэширование страниц веб-браузером (и прокси-серверами). Основной способ достижения этого использует мета-теги HTML:

Вставив прошедшую дату в мета-тег Expires, вы сообщаете браузеру, что кэшированная копия странички всегда является устаревшей. Это значит, что браузер никогда не должен кэшировать страницу. Мета-тег Pragma: no-cache – это довольно хорошо поддерживаемое соглашение, которому следует большинство веб-браузеров. Обнаружив этот тег, они обычно не кэшируют страницу (хотя никаких гарантий нет, это всего лишь соглашение). Это хорошо звучит, но есть две проблемы, связанные с использованием мета-тегов:

1. Если тег не существовал, когда страница была запрошена браузером впервые, но появляется позже (например, вы модифицировали включаемый файл pageheader.php, который является шапкой каждой веб-страницы), браузер останется в блаженном неведении и воспользуется свей кэшированной копей оригинала.

2. Прокси-серверы, кэширующие веб-страницы, вообще не будет исследовать непосредственно содержимое HTML-документа. Вместо этого они полагаются только на веб-сервер, с которого пришли документы, и протокол HTTP. Иными словами, браузер может считать, что не должен кэшировать страницу, но прокси-сервер между браузером и веб-сервером вероятно не знает этого – и продолжит отправлять клиенту ту же самую страницу.

Лучший подход состоит в том, чтобы использовать непосредственно протокол HTTP с помощью функции PHP header:


header("Pragma: no-cache");
?>

Мы можем пойти на один шаг вперед, воспользовавшись заголовком Cache-Control, совместимым с браузерами, поддерживающими HTTP 1.1:

header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", FALSE);
header("Pragma: no-cache");
?>

Это гарантирует, что никакой веб-браузер или промежуточный прокси-сервер не будет кэшировать страницу, таким образом, посетители всегда получат самую последнюю версию контента. Фактически, первый заголовок должен быть самодостаточным, это лучший способ гарантировать, что страница не кэшируется. Заголовки Cache-Control и Pragma добавлены с целью подстраховки. Хотя они не работают во всех браузерах или прокси, они отловят некоторые случаи, в которых Expires не работает должным образом (например, если дата на компьютере клиента установлена неправильно). Конечно, полный отказ от кэширования обеспечивает нас проблемами, которые мы обсуждали в начале этой главы. Сейчас мы рассмотрим решение этих проблем.

Internet Explorer и кэширование загрузки файлов

Проблемы могут возникать, когда вы имеете дело с кэшированием и загрузкой файлов. Если при обслуживании загрузки файла PHP-скриптом используются такие заголовки, как, например Content-Disposition: attachment, filename=myFile.pdf или Content-Disposition: inline, filename=myFile.pdf, у вас будут проблемы с Internet Explorer’ом, если вы сообщите браузеру не кэшировать страницу.

Internet Explorer оперирует загрузкой довольно необычным образом, выполняя два запроса к веб-сайту. Первый запрос загружает файл и сохраняет его в кэше, пока не будет создан второй запрос (без сохранения отклика). Этот запрос вызывает процесс передачи файла конечному пользователю в соответствии с типом файла (например, запускает Acrobat Reader, если файл является PDF-документом). Это значит, что если вы отправили заголовки, запрещающие браузеру кэшировать страницу, Internet Explorer удалит файл между первым и вторым запросом, в результате чего конечный пользователь ничего не получит. Если файл, который вы отдаете PHP-скриптом, не изменяется, одним из простейших решений будет убрать«запрещающие кэширование заголовки из скрипта.

Если загружаемый файл регулярно изменяется (то есть вы хотите, чтобы браузер загружал новейшую версию), вы должны использовать заголовок Last-Modified и гарантировать, что время модификации между двумя последовательными запросами не изменяется. Вы должны сделать это таким образом, чтобы не повлиять на пользователей браузеров, правильно оперирующих загрузкой. Одним из решений в этом случае будет сохранение файла на вашем веб-сервере и указание простой ссылки к нему, предоставляя веб-серверу сообщать за вас заголовки кэширования. Конечно, это решение не может быть приемлемым, если предполагается авторизованный доступ к файлу, поскольку допускает непосредственную загрузку сохраненного файла.

как я могу захватить данные на стороне сервера для кэширования?

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

Несколько слов о кэшировании при помощи шаблонов. Наличие шаблонных движков типа Smarty часто говорит о кэшировании шаблонов. Обычно эти движки предлагают встроенный механизм для сохранения откомпилированной версии шаблона (то есть генерируют из шаблона PHP-исходник), что предохраняет нас от необходимости парсить шаблон каждый раз, когда запрашивается страница. Это не нужно путать с кэшированием вывода, которое имеет отношение к кэшированию предоставляемого HTML (или другого вывода), который посылает PHP в браузер. Вы можете успешно использовать оба типа кэширования одновременно на одном и том же сайте.

Сейчас мы рассмотрим встроенный механизм кэширования на PHP, использующий буферизацию вывода, который может использоваться вами независимо от способа создания контента (с шаблонами или без шаблонов). Рассмотрим ситуацию, в которой ваш скрипт отображает результат, используя, к примеру, echo или print, чтобы выдать данные непосредственно в браузер. В таком случае вы можете использовать функции управления выводом PHP для хранения данных в буферной памяти, над которой ваш PHP-скрипт имеет контроль.
Вот простой пример:


ob_start();
// Выводим некоторый текст (который сохраняется в буфере);
echo "1. Выводим это в буфер
";
// Получаем содержимое буфера
$buffer = ob_get_contents();
// Останавливаем буферизацию и очищаем буфер вывода
ob_end_clean();
// Выводим некоторый текст обычным образом
echo "2. Нормальный вывод
";
// Вывод содержимого буфера
echo $buffer;
?>

Сам буфер хранит вывод как строку. Так, в приведенном скрипте мы начинаем буферизацию с ob_start и используем echo, чтобы вывести что-либо. Затем мы используем ob_get_contents, чтобы выбрать данные, помещенные в буфер оператором echo, и сохранить их в строке. Функция ob_end_clean останавливает буферизацию вывода и уничтожает его содержимое. Как альтернативу можно использовать ob_end_flush, чтобы вывести содержимое буфера. Вышеописанный скрипт выведет:

2. Нормальный вывод
1. Выводим это в буфер

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

заголовки HTTP и буферизация вывода

Буферизация вывода может помочь решить наиболее общую проблему, связанную с функцией header, не говоря уже о session_start и set_cookie. Обычно, если вы вызываете любую из этих функций после того, как начался вывод страницы, вы получите противное сообщение об ошибке. При включенной буферизации вывода единственным типом вывода, избегающим буферизации, являются HTTP-заголовки. Используя ob_start в самом начале выполнения вашего приложения, вы можете посылать заголовки в любой понравившейся точке программы, не сталкиваясь с обычными ошибками. Затем, как только вы будете уверены, что больше выводить HTTP-заголовки не потребуется, вы можете сразу же вывести содержимое страницы из буфера.

/* Cледует заметить, что подобное использование данной функции неоправданно. В большинстве случаев необходимости в использовании буферизации вывода для избавления ошибок указанного типа просто не существует и все с легкостью может быть исправлено правильным проектированием приложения - прим. переводчика */

использование буферизации вывода для кэширования на стороне сервера

Вы уже видели базовый пример буферизации вывода, теперь следующий шаг, в котором буфер сохраняется в файл:

// Если существует кэшированная версия…
if (file_exists("./cache/2.cache")) {
// Читаем и выводим файл
readfile("./cache/2.cache");
exit();
}
// Начинаем буферизацию вывода
ob_start();
// Выводим остальной HTML
?>
>http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
>http://www.w3.org/1999/xhtml">

Кэшированная страница

Эта страница кэшируется средствами PHP
Функции>http://www.php.net/outcontrol">Функции управления выводом

// Получаем содержимое буфера
$buffer = ob_get_contents();
// Останов буферирования и вывод буфера
ob_end_flush();
// Сохранение кэш-файла с контентом
$fp = fopen("./cache/2.cache", "w");
fwrite($fp, $buffer);
fclose($fp);
?>

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

блочная буферизация

При упрощенном подходе мы кэшируем выводимый буфер как одну страницу. Однако этот подход лишает реальных возможностей, предоставляемых функциями управления выводом PHP.
Вне всякого сомнения, некоторые части отправляемой посетителю страницы изменяются очень редко, например, такие как шапка, меню и нижний колонтитул. Однако другие части, типа таблиц, содержащих обсуждения в форуме, могут изменяться довольно часто. Буферизация вывода может использоваться к кэшированию разделов страницы в отдельных файлах, затем создавать из них страницу – решение, устраняющее необходимость повторных запросов к базе данных, циклов while и т.д. Вы можете назначать каждому блоку страницы дату истечения срока, после которой пересоздается кэш-файл, или, кроме того, вы можете включить в ваше приложение механизм, который будет удалять кэш-файл каждый раз, когда сохраненный в нем контент изменен.
Вот пример, демонстрирующий этот принцип:

/
* Запись кэш-файла
* @param string contents – содержание буфера
* @param string filename – имя файла, используемое при создании кэш-файла
* @return void
*/
function writeCache($content, $filename) {
$fp = fopen("./cache/" . $filename, "w");
fwrite($fp, $content);
fclose($fp);
}
* Проверка кэш-файлов
* @param string filename – имя проверяемого кэш-файла
* @param int expiry – максимальный «возраст» файла в секундах
* @return mixed содержимое кэша или false
*/
function readCache($filename, $expiry) {
if (file_exists("./cache/" . $filename)) {
if ((time() - $expiry) >filemtime("./cache/" . $filename))
return FALSE;
$cache = file("./cache/" . $filename);
return implode("", $cache);
}
return FALSE;
}
?>

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

В этом примере я использовал процедурный подход. Однако я не советую делать это на практике, поскольку это закончится очень грязным кодом (смотри последующие решения с лучшей альтернативой) и, вероятно, вызовет проблемы с блокировкой файла (например, что случится, когда кто-то обращается к кэшу в момент его обновления?).
Давайте продолжим этот пример. После того, как запущена буферизация вывода, начинается обработка. Сначала скрипт вызывает readCache, чтобы узнать, существует ли файл 3_header.cache - он содержит шапку страницы, то есть заголовок HTML и начало тела. Мы используем функцию date чтобы вывести время, когда страница фактически была сгенерирована, таким образом вы увидите различные кэш-файлы в работе, когда страница будет отображена.

// Начинаем буферизацию вывода
ob_start();
// Обработка шапки
if (!$header = readCache("3_header.cache", 604800)) {
// Вывод шапки
?>
>http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
>http://www.w3.org/1999/xhtml">

Страница, кэшированная поблочно

Время создания шапки:

$header = ob_get_contents();
ob_clean();
writeCache($header,"3_header.cache");
}
?>

Что же случается когда кэш-файл не найден? Выводится некоторый контент и присваивается переменной при помощи ob_get_contents, после чего буфер очищается функцией ob_clean. Это позволяет нам перехватывать вывод по частям и сопоставлять их с индивидуальными кэш-файлами при помощи writeCache. Заголовок страницы теперь хранится как файл, который может быть использован без нашего вмешательства в пересборку страницы. Давайте вернемся на секунду к началу условного оператора. Когда мы вызывали readCache, мы передали ей время жизни кэша в 604800 секунд (одна неделя), readCache использует время модификации кэш-файла, чтобы определить, является ли кэш-файл все еще допустимым.

Для содержимого (тела) страницы мы по-прежнему будем использовать тот же процесс. Однако на сей раз при вызове readCache мы будем использовать время жизни кэша в пять секунд, кэш-файл будет модифицироваться каждый раз, когда он «старше» 5 секунд:

// Обработка тела страницы
if (!$body = readCache("3_body.cache", 5)) {
echo "Время создания тела: " . date("H:i:s") . "
";
$body = ob_get_contents();
ob_clean();
writeCache($body, "3_body.cache");
}
?>

Нижний колонтитул эффективно изменять так же, как заголовок.
Конечный результат выглядит примерно так:
- время создания шапки - 17:10:42;
- время создания тела - 18:07:40;
- время создания нижнего колонтитула - 17:10:42.
Заголовок и нижний колонтитул обновляются еженедельно, в то время как тело модифицируется, когда оно старее 5 секунд.

вложенные буферы

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

Как мне реализовать простую систему кэширования на стороне сервера?
Теперь, когда мы понимаем идеи буферизации вывода, пришло время рассмотреть, как мы можем использовать этот процесс в действии таким образом, чтобы его было легко поддерживать. Чтобы сделать это, мы воспользуемся небольшой помощью от PEAR::CacheLite.

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

Cache_Lite состоит из трех основных классов. Первым является базовый класс Cache_Lite, который отвечает только за создание и чтение кэш-файлов и не занимается буферизацией вывода. Данный класс можно использовать в одиночку в тех случаях, когда нет необходимости использовать буферизацию вывода, как, например, при сохранении результата разбора шаблона PHP-скриптом. Приведенные здесь примеры не используют класс Cache_Lite напрямую и демонстрируют применение остальных двух классов. Cache_Lite_Function используется для вызова функции или метода класса и последующего кэширования результатов работы. Это может оказаться полезным, например, для кэширования результата запроса к MySQL. Класс Cache_Lite_Output использует PHP-функции контроля вывода для перехвата данных, сгенерированных скриптом, и сохранения их в кэш-файлах. Это позволяет выполнять те же задачи, что и предыдущее решение.

настройки Cache_Lite

В текущей версии класса (1.1) доступны следующие настройки:
- cacheDir - каталог, в который будут помещаться файлы кэша. Значение по умолчанию - каталог, где выполняется скрипт;
- caching - эта опция включает или выключает возможности Cache_Lite. Например, если у вас очень много запросов к Cache_Lite, а в процессе отладки вы захотите выключить кэширование, установить в FALSE. Значение по умолчанию - TRUE.
lifetime - параметр содержит в себе заданный по умолчанию отрезок времени жизни кэша (в секундах);
- fileNameProtection - использование MD5-кодирования для генерации имени файла с кэшем. Это позволяет вам использовать в названии файлов кэша и групп любые символы, даже запрещенные файловой системой;
- fileLocking - включает механизмы блокирования файла с кэшем на время записи в него данных;
- writeControl - проверяет, что файл кэша был записан правильно сразу после окончания записи;
- readControl - перед чтением файла с кэшем проверяет его на искажения;
- readControlType - этот параметр определяет тип механизма чтения файлов кэша. Доступные механизмы: цикличная проверка избыточности, MD5-хэш или простая проверка длинны. Обратите внимание, что этот механизм не предназначен для защиты файлов кэша от вызова их напрямую посторонними пользователями. Это всего лишь способ определить - испорчен файл или нет;
- pearErrorMode - включает принятый в PEAR способ возврата ошибок;
- memoryCaching – каждый раз, когда вы вызываете запись кэша в файл, он записывается в массив Cache_Lite. saveMemoryCachingState и
getMemoryCachingState используются для доступа к кэшу, сохраненному в памяти между запросами. Преимущество подобного метода состоит в том, что содержимое кэша может быть сохранено в едином файле, что сокращает число циклов чтения/записи на диск. Кэш восстанавливается прямо в массив, к которому ваш скрипт имеет доступ;
memoryCachingLimit - парметр определяет предел количества файлов кэша, которые могут быть сохранены в массиве в памяти.

очистка кэша

Cahce_Lite содержит в себе удачный механизм определения времени жизни файлов кэша, что создает хорошую основу для сохранности ваших файлов и их своевременного обновления. Однако бывают моменты, когда вам требуется немедленное обновление файла кэша. Для таких случаев существует методы remove() и clean(). Метод remove() предназначен для удаления конкретного файла кэша. ему требуется ID кэша и название группы, в которую входит файл. Следующий пример удалит файл с кэшем тела (body) из предыдущего примера:

$cache->remove("body", "Dynamic");

Метод clean() позволяет удалить все файлы в нашем каталоге, если вызвать его без параметров. Если в качестве параметра передать название группы, то будут удалены все файлы, входящие в данную группу. Если бы мы хотели удалить заголовок и нижнюю часть, то нужно подать команды, аналогичные этой:

$cache->clean("Static");

Методы remove() и clean() нужно, очевидно, вызывать в ответ на события, в пределах приложения. Например, если у вас есть форум, наверняка следует удалить файл кэша, если какой-либо пользователь отправит новое сообщение. Несмотря на то, что это решение выглядит красиво, оно может повлечь за собой изменение кода. Если у вас есть главный скрипт, который подключается к каждой странице приложения, которую может просмотреть посетитель, вы можете просто наблюдать за поступающими событиями, например, за переменной $_GET["newPost"], удаляя требуемые файлы кэша. Это позволит вам создать централизованный механизм управления кэшем. Вы могли бы даже включить этот код в php.ini.

кэширование вызовов функций

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

PEAR Web installer использует в своей работе Cache_Lite для кэширования XML-RPC запросов, передаваемых PEAR Web серверу.
Вот код, который получает данные от удаленного сервера:

$countries = $stationInfo->listCountries();

$country = $stationInfo->searchByCountry($_GET["country"]);

В обоих случаях эти вызовы соответствуют запросу данных по сети. Используя Cache_Lite, мы могли бы кэшировать данные, возвращаемые сервисом, и могли бы использовать их многократно. Это позволило бы избежать дополнительных ненужных сетевых соединений и значительно увеличило бы скорость выполнения. Обратите внимание, что здесь мы рассматриваем только код, касающийся нашей темы. В начале мы подключаем Cache_Lite_Function:

// Include PEAR::Cache_Lite_Function
require_once "Cache/Lite/Function.php";

// Задаем параметры для for Cache_Lite_Function
// ВНИМАНИЕ: fileNameProtection = TRUE!
$options = array(
"cacheDir" =>"./cache/",
"fileNameProtection" =>TRUE,
"writeControl" =>TRUE,
"readControl" =>TRUE,
"readControlType" =>"strlen",
"defaultGroup" =>"SOAP"
);
// Создаем объект класса Cache_Lite_Function
$cache = new Cache_Lite_Function($options);

Важно, что параметр fileNameProtection установлен в TRUE. Это значение принято по умолчанию, однако я специально выставил его вручную, чтобы подчеркнуть значимость. Если этот параметр установить в FALSE, получится недопустимое имя файла, так что кэширования не будет.
Далее, мы делаем запрос к нашему клиенту SOAP:

$countries = $cache->call("stationInfo->listCountries");
$country = $cache->call("stationInfo->searchByCountry",
$_GET["country"]);

Если запрос делается впервые, Cache_Lite_Function хранит результаты в виде сериализованного массива в файле кэша (вы не должны об этом волноваться), и этот файл будет использоваться для будущих запросов, пока не истечет время его жизни. setLifeTime может использоваться, чтобы определить, как долго будет жить файл кэша перед обновлением информации в нем. Значение по умолчанию - 1 час (3600 секунд).

PEAR::Cache

Вообще, Cach_Lite обеспечивает единую, простую в использовании систему, чтобы решить любые вопросы, связанные с кэшированием. Поскольку следующий уровень - это сайты с особенно большим трафиком, вам стоит разобраться с PEAR::Cache – «старшим братом» Cache_Lite. Он также предусматривает расширение возможностей кэширования, например, кэширование в общей памяти, как альтернативу кэширование в файл, или помощь Msession PHP extension, храня данные в сбалансированной сессии, которая является особенно полезной в сбалансированных веб-серверах. Cache_Lite, однако, предлагает более чем достаточные возможности, и отвечает потребностям большинства сайтов.

как управлять кэшированием на стороне клиента средствами PHP?

После того, как мы рассмотрели варианты отмены кэширования на стороне клиента, пришло время посмотреть на механизм, который позволит нам контролировать кэш на стороне клиента средствами PHP. Этот подход будет работать только если вы используете PHP в связке с сервером Apache, поскольку мы будем использовать функцию getallheaders, чтобы получить заголовки, передаваемые браузером. Эта функция работает только в Apache. Если вы используете PHP 4.3.0 с Apache, работа с HTTP-заголовками возможна с помощью функций apache_request_headers и apache_response_headers. Функция getallheaders стала псевдонимом для новой функции apache_request_headers.

Механизмом для работы с кэшем веб-браузера вновь является HTTP. Множество заголовков вовлечены в инструктирование веб-браузеров и прокси-серверов независимо кэшировать страницу. Ситуация осложняется тем фактом, что некоторые из них доступны только с HTTP 1.1.

проверка HTTP-заголовков в вашем браузере

Простым, но очень удобным инструментом для проверки заголовков запросов и откликов является LiveHttpHeaders – аддон к браузеру Mozilla. Необходимо точно знать, какие заголовки посылает ваш скрипт, особенно когда вы имеете дело с заголовками кэширования HTTP.
Для простоты мы рассмотрим только заголовки кэширования HTTP 1.0, а именно Expires, Last-Modified и If-Modified-Since, а также статус-код HTTP 304 (Not Modified).

Другие заголовки, доступные с HTTP 1.1, например Cache-Control и ETag, предназначены для обеспечения расширенного механизма, который может использоваться совместно с состоянием веб-сессии, иными словами, версия одной и той же страницы, предназначенная для неавторизованного посетителя, может значительно отличаться от отображаемой авторизованному пользователю. Заголовки HTTP 1.1 изначально добавлялись для того, чтобы позволить кэшировать такие страницы.

истечение срока жизни страницы

Самым простым в использовании заголовком является заголовок Expire, который устанавливает дату, когда страница устареет. До этого момента веб-браузеру разрешается использовать кэшированную версию страницы.
Пример:


function setExpires($expires){
header("Expires: " . gmdate("D, d M Y H:i:s", time() + $expires) . "GMT");
}
echo "Эта страница самоуничтожится через 10 секунд
";
echo "Сейчас " . gmdate("H:i:s") . " GMT
";
echo "
";
?>

Вышеприведенный пример показывает текущее время по Гринвичу и выводит ссылку, которая вам позволяет перейти на страницу вновь. Используя кнопку Refresh вашего браузера, вы можете сообщить браузеру о желании обновить кэш.

даты и время в HTTP

Даты в HTTP всегда вычисляются относительного меридиана времени Гринвича (GMT). Функция PHP gmdate - точно такая же функция, как date, за исключением того, что она автоматически компенсирует время по Гринвичу, основанное на системных часах и настройках региона вашего сервера. Когда браузер сталкивается с заголовком Expires, он кэширует страницу. Все последующие запросы страницы, сделанные до указанного времени истечения срока жизни, используют версию страницы из кэша, никаких запросов к веб-серверу при этом не происходит.
Заголовок Expires в принципе прост в реализации, но в большинстве случаев, если вы не высокоорганизованный человек, вы не можете знать точно, когда данная страница вашего сайта будет обновлена. Поскольку браузер войдет в контакт с сервером только после того, как страница устареет, нет ни одного способа сообщить браузеру, что страница, находящаяся в его кэше, устарела.

время изменения страницы

Более практично использовать заголовки Last-Modified и If-Modified-Since, доступные в HTTP 1.0. При использовании этого метода вы должны отправлять заголовок Last-Modified в ответ на каждый запрос к вашему PHP-скрипту. При следующем запросе страницы браузером, он отправит заголовок If-Modified-Since, содержащий время, по которому ваш скрипт может определить, обновлялась ли страница со времени последнего запроса. Если это не так, ваш скрипт посылает код статуса HTTP 304, чтобы указать, что страница не изменялась, не выводя при этом содержимого страницы. Если вы объедините подход времени последнего изменения со значением времени, являющимся уже доступным в вашем приложении (например, время самой последней новостной статьи), вы сможете воспользоваться преимуществами кэша веб-браузера и разгрузите канал передачи данных, по возможности сэкономив информационный трафик с вашего сайта и улучшив его производительность.

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

Harry Fuecks, перевод Муллина Сергея (SiMM) и Кузьмы Феськова.

В предыдущем материале о веб-технологиях мы упомянули полезную статью Кэширование в HTTP (далее: «Статья с nomagic.ru»). По статье, однако, у нас возникли некоторые вопросы, а обсуждение там заглохло, поэтому пришлось искать все ответы самим. Вопросы, собственно, не именно по статье – они копились в течение нескольких лет. Надоело иметь их нерешёнными, а статья просто дала повод искать решения более активно.

Инструменты

Первый вопрос – как увидеть HTTP-заголовки запросов браузера и ответов сервера? Автор статьи с nomagic.ru рекомендует использовать для этой цели "Web Developer tools" в Firefox"е и какой-то мутный "DevToolbar" для ИЕ. Рука потянулась было кликнуть по ссылке, но зависла в воздухе:

1) Web Developer tools для FF у нас и так есть, и там нет инструмента для просмотра HTTP-заголовков, там даже DOM-инспектор в версии 3 зачем-то убрали!

3) И совсем мрачная мысль: ладно, допустим, для FF у нас есть-таки LiveHTTPHeaders; с ИЕ – вдруг да повезёт; ну, а Опера? А Google chrome?.. Нам что теперь, по всему огороду метаться?

Почему бы прямо на сайте, средствами PHP не отобразить все HTTP-заголовки? Там ведь есть переменные окружения , переменные для работы с сервером и всё такое. То есть точно известно, что там есть, например, $_SERVER["HTTP_HOST"] и HTTP_REFERER (у нас на каждом сайте используются). Надо добавить все остальные HTTP_ – вот и будут заголовки запроса. Тем более, что в PHP для этого есть специальная функция getallheaders(). Или apache_request_headers(). И apache_response_headers(). Да. Так можно вывести на экран все HTTP-заголовки. Казалось бы. Но нас ожидал тяжёлый удар ниже пояса и 15-минутные мучения, результатом которых стало открытие: на нашем хостинге PHP установлен как cgi (а не как модуль Апач) && в такой конфигурации все эти функции...headers() не работают!

Запустив скриптик с echo phpinfo() и бегло просмотрев результат, обнаруживаем, что искомые заголовки HTTP-запроса есть в массиве $_ENV (и больше нигде). Ладно, _env так _env. Но там много всякого хлама (в данный момент для нас лишнего), поэтому создаём новый массив $varrvis и аккуратно откромсываем туда из _env более-менее нужные куски:

Foreach($_ENV as $ke=>$va) { if (preg_match("/^HTTP\_/i",$ke) && !preg_match("/COOKIE/i",$ke)) $varrvis["$ke"]=$va; }

А вот получить заголовки ответа нашего сервера – ну ваще никак, кроме функции headers_list() . И только те заголовки, которые мы сами отправим в скрипте PHP с помощью функции header() . По идее функцию headers_list() следует запускать после написания всех заголовков. Мы так примерно и сделали, хотя, скорее всего, для данного сайта (сайт – на котором ставились опыты) это без разницы, потому что везде используется ob_start("ob_gzhandler") . В конец тестируемых скриптов добавляем конструкцию:

Foreach(headers_list() as $ke=>$va) { $varrvis[$ke]=$va; }

И дополняем наш массив заголовков ответами сервера. А между Запросом и Ответом для удобства чтения вставим строку:

$varrvis["Response"]="==============================";

Осталось в самом конце тестируемых скриптов написать print_r($varrvis) – и потом бодро листать страницы сайта во всех подручных браузерах, любуясь HTTP-заголовками.

HTTP-кэширование инструкциями Apache

В статье с nomagic.ru указывается два источника инструкций кэширования: конфигурационные файлы Апача (http.conf && .htacces) и непосредственно PHP-скрипт с командами вида header("Pragma: no-cache"). Но существует ещё третий источник – его можно обнаружить несложным опытом:

1) пишем (раскомментируем) в httpd.conf (Апач 1.3.39) cтроки:

LoadModule expires_module modules/mod_expires.so LoadModule headers_module modules/mod_headers.so AddModule mod_expires.c AddModule mod_headers.c

2) в папке нашего сайта в .htaccess добавляем инструкции:

Header append Cache-Control "public" ExpiresActive On ExpiresDefault "access plus 1 hours"

3) пишем простенький скрипт pi.php из двух строк:

4) открываем страницу pi.php в Firefox и видим в LiveHTTPHeaders (Наш PHP «инструмент» может показывать только заголовки, отправленные функцией header(), а пока мы ей не пользуемся). следующие строки, имеющие отношение к кэшированию:

Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Expires: Thu, 19 Nov 1981 08:52:00 GMT Pragma: no-cache

Вуаля. И не надо никакой Википедии – вот они заголовки, убивающие кэширование. Они исходят из третьего источника – файла php.ini. В нём по умолчанию, при установке PHP записана, в частности, следующая инструкция:

Session.cache_limiter = nocache

Именно она заставляет PHP посылать анти-кэширующие заголовки в определённых условиях (например, при использование функции session_register()). Мы, конечно, немного схитрили, подогнав ситуацию под эти условия. Но кто поручится, что никогда не будет использовать в своих скриптах функцию session_register() ? Да, в общем-то, и без неё дело обстоит достаточно хреново: уберите первую строку из скрипта pi.php (оставив только echo phpinfo();) – тоже ничего хорошего:

И это всё, что дают кэширующие инструкции Апача в сочетании с "session.cache_limiter=nocache" в php.ini. Отсутствует самый главный заголовок – Last-modified (дата последнего изменения страницы), без которого невозможно ни правильно установить, ни правильно уничтожить кэширование в браузере.

Самый забавный результат получается, если "дёрнуть попугая сразу за обе ноги" – написать в php.ini "session.cache_limiter=private" (нужна перезагрузка Апача) и оставить в скрипте строку session_register("var1"):

Cache-Control: private, max-age=300, pre-check=300 Expires: Thu, 19 Nov 1981 08:52:00 GMT Last-Modified: Mon, 06 Jul 2009 15:13:40 GMT

Появляется Last-Modified , который показывает время последнего изменения скрипта php, а Cache-Control противоречит Expires . Поведение браузеров будет непредсказуемым.

Правильное HTTP-кэширование

В последних полученных нами в ходе опыта заголовках противоречивость в общем-то совсем не фатальна: браузеры и не такое ещё видали, к таким вещам они вполне «помехоустойчивы». Наибольшую проблему представляет как раз Last-Modified , который нужен и пользователям (браузерам), и поисковикам. Понятно, что дату изменения файла PHP для него использовать нельзя – потому что реальное содержимое страницы может быть вообще никак не связано с этой датой: обычно содержимое страницы извлекается из базы данных, и дату его изменения тоже надо извлекать оттуда (из БД ).

Если это конкретная статья с нашего сайта, мы просто берём дату текущей записи из поля `datrec` таблицы `articles`. Если это список статей (на главной странице сайта), мы ищем наибольшую дату всех записей по формуле "select max(`datrec`) from `articles`" – именно она и будет датой последнего изменения страницы, которую мы передадим в заголовке Last-Modified .

Существуют ещё две «точки контроля» содержимого, реализуемые с помощью HTTP-заголовков:

1) Etag – хэш содержимого страницы, получаемый, например, с помощью функции md5 (текст_страницы);

2) Content-length – общая длина текста, отправленного браузеру в ответ на его запрос.

Мы не можем использовать Content-length , потому что этот параметр постоянно меняется: в правой колонке каждой страницы у нас висит напоминание о том, что это всё-таки сайт рекламной газеты «Деловая неделя», – список товаров последнего номера газеты. Список этот довольно большой, поэтому на странице выводится только небольшая часть списка, выбранная случайным образом .

Как же, спросите вы, мы используем Etag – он ведь тоже тогда постоянно случайным образом меняется? А очень просто: мы не включаем переменную часть страницы в хэш, а составляем хэш только «по материалам базы данных статей». Почему же нельзя так же поступить и с Content-length ? Да потому что Content-length браузер может легко проверить (ИЕ так и делает – отправляет на сервер обратно действительную длину полученного содержимого). А хэш можно написать какой попало (главное, чтобы он менялся при изменении отслеживаемой части страницы), браузер ведь не знает, какой мы используем алгоритм, и вынужден просто принимать наш Etag на веру.

Мы используем два способа хэширования:

1) в случае списка текстов, получаемых из многих строк таблицы, создаём Etag * по формуле $etag=md5($list) ;

2) в более простом случае (извлекается только одна запись из таблицы) заставляем работать mysql, добавляя в запрос лишнее значение: "select `id`, `title`, `text`, `author`, `datrec`, old_password(concat(`title`,`text`,`author`)) as `etag` from `articles`...".

При отправке заголовков функцией header() нужно следить, чтобы эти действия производились раньше какой-либо отправки содержимого браузеру (через echo, print PHP или просто обычным HTML-кодом). То есть сначала вся проверяемая часть помещается в переменную, вычисляется Etag *, отправляются все заголовки, и только потом можно выводить содержимое. Если вы, конечно, не написали в начале страницы ob_start("ob_gzhandler"). Мы-то как раз написали, поэтому отправляем заголовки как попало и когда попало. Вот этот ob_gzhandler ещё позволяет получить всё содержимое, отправляемое браузеру, сразу – функцией ob_get_contents() , а также истинную длину содержимого (для заголовка Content-length ) – функцией ob_get_length() . Мы, как уже говорили, не можем на данном сайте использовать всё содержимое страницы для формирования этих заголовков. Но на других сайтах – вполне.

304 Not Modified

Итак, мы отправляем клиентам правильную дату изменения страницы и Etag . Клиенты относятся с пониманием – посылают в следующих обращениях к этой странице заголовки If-Modified-Since и If-None-Match , что вы можете увидеть сами в самом низу любой нашей статьи (после нажатия клавиши F5, разумеется). Но желанный результат не достигнут: сервер в ответ на все запросы браузера исправно посылает заголовок HTTP/1.x 200 OK , и никаких 304 . Наш «инструмент» не отображает заголовки "200 OK", потому что мы их не формируем функцией header().

Заголовок 304 можно увидеть в большом количестве через LiveHTTPHeaders – у файлов картинок, Javascript, css и простых HTML страниц. Этот заголовок отправляет сам Апач, и он делает это без всяких наших ухищрений с модулем headers.so и без дополнительных инструкций типа "ExpiresActive On". Но не для страниц, формируемых PHP.

Мы сами вписали в PHP-скрипт отправку заголовков браузеру, и сами должны проверять на наличие-отсутствие валидации последующие запросы браузера, и сами же потом сличать контрольные параметры и, в зависимости о результата, отправлять браузеру заголовок 200 или 304. Точнее, заголовок 200 PHP отправляет всегда сам, нам нужно только вычислять ситуацию необходимости 304. Мы делаем это в главном конфигурационном файле всех сайтов configbase.php.

Сложность получения информации о заголовках в том, что на одном хостинге PHP работает как cgi, а на другом как модуль Апач, поэтому сначала приходится проверять наличие переменных в «суперглобальных» массивах Env и Server , и в зависимости от результат создавать ссылку на подходящий массив:

$h304="HTTP/1.x 304 Not Modified"; $match=""; $since=""; $varr=array(); $varrvis=array(); if (array_key_exists("HTTP_HOST",$_ENV)) $varr =& $_ENV; if (array_key_exists("HTTP_HOST",$_SERVER)) $varr =& $_SERVER; if (isset($varr["HTTP_IF_NONE_MATCH"])) $match=$varr["HTTP_IF_NONE_MATCH"]; $match=trim(strval($match)); if (isset($varr["HTTP_IF_MODIFIED_SINCE"])) $since=$varr["HTTP_IF_MODIFIED_SINCE"]; $since=explode(";",$since); $since=strtotime(trim($since));

Предпоследняя строчка нужна из-за ИЕ, который в заголовке IF_MODIFIED_SINCE отправляет ещё и длину страницы: "Fri, 03 Jul 2009 15:42:30 GMT; length=20994" – мы отрезаем от данного заголовка всё, что может быть после точки с запятой. Затем создаём независимый от конкретного хостинга массив HTTP-заголовков:

Foreach($varr as $ke=>$va) { if (preg_match("/^HTTP\_/i",$ke) && !preg_match("/COOKIE/i",$ke)) $varrvis["$ke"]=$va; } $varrvis["Response"]="=============================";

Ну, и главный фрагмент кэширования, ядро всей нашей системы, находящееся внутри страниц PHP (где $dat – время из таблицы mysql, переведённое в секунды функцией strtotime ):

Header("Etag: $etag"); header("Cache-Control: private, max-age=0"); header("Expires: ".gmdate("r")." GMT"); header("Connection: Keep-Alive"); header("Keep-Alive: timeout=5, max=100"); if ($since==$dat) { if (!$match || $match==$etag){ $varrvis=$h304; include "bottom.php"; header($h304); header("Connection: Close"); exit; } } else { header("Last-Modified: ".gmdate("r", $dat)." GMT"); }

Система работает корректно во всех упомянутых в данной статье браузерах: кэширует, когда это нужно, и отправляет браузеру новую информацию, если она есть. Например, если после открытия главной страницы сайта (со списком статей) нажать F5 (не в Опере!:-), внизу страницы можно увидеть долгожданный заголовок 304 (в Опере его тоже можно увидеть, если попасть на данную страницу, щёлкнув по ссылке на другой странице сайта). Если в заголовок какой-нибудь статьи были внесены изменения или, например, добавилась новая статья, скрипт, получив от браузера запрос валидации, обнаружит изменение данных и отправит браузеру новое содержимое страницы, а не заголовок 304 .

Человеческими словами то, что мы делаем с помощью этих заголовков, можно пересказать так:

1) мы посылаем браузеру (вообще любому клиенту) две метки идентификации: время последнего изменения содержимого старницы и хэш страницы (контрольную сумму); мы посылаем также инструкцию, разрешающую кэширование только конечному клиенту (Cache-Control: private); в этом же заголовке (max-age=0) мы говорим о том, что клиент не должен запрашивать новое содержимое в течение 0 секунд (то есть должен запрашивать вего всегда); в следующем заголовке (Expires) мы говорим клиенту то же самое: срок «сгорания» актуальности страницы истекает немедленно, прямо сейчас;

2) браузер послушно складывает страницу в свой кэш, вместе с картинками и файлами css; при последующих обращениях к странице браузер спрашивает у сервера, изменилась ли дата (IF_MODIFIED_SINCE) и, иногда, контрольная сумма (IF_NONE_MATCH) – про контрольную сумму ИЕ, например, не спрашивает;

3) если дата изменилась, мы проверяем, был ли от браузера запрос контрольной суммы, и если был, проверяем также её изменение; если ничего не поменялось, отправляем браузеру заголовок 304 ; если поменялось – не отправляем 304 (и PHP сам отправляет 200 OK);

Да, и ещё одна деталь для нашего «инструмента»: первый заголовок (HTTP-статуса) почему-то никак не извлекается функцией headers_list() . Когда он 200 , это не очень принципиально, но 304 хотелось бы видеть (чтобы убедиться в работоспособности нашей системы кэширования). Поэтому приходится «подрисовывать» этот заголовок в массив заголовков руками в строке

$varrvis=$h304; ,

а потом для всех остальных полученных функцией headers_list() заголовков увеличить индекс на единицу ($ke+1):

Foreach(headers_list() as $ke=>$va) { $varrvis[$ke+1]=$va; }

Последний нюанс. Как увидеть заголовок 304 в браузере, если браузер получил этот заголовок от сервера, и не получил никакого содержимого страницы (страница не должна меняться на экране)? Пусть это останется нашей маленькой тайной.

© 2009, «Деловая неделя», Михаил Гутентог

501 . SlipkeR

Спасибо) все понятно и доходчиво написано) автору спс)

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

Шаг первый. Создаем файл top-cache.php

Нам нужно создать два файла. Первый: создаем файл с именем top-cache.php и копируем в него следующий код:

\n"; include($cachefile); exit; } ob_start(); // Запуск буфера вывода?>

Что происходит в данном коде? Первые 5 строк создают имя файла кеша в соответствии с текущем PHP файлом. Например, если мы используем файл с именем list.php , файл кеша будет иметь вид cached-list.html .

Строка 6 создает переменную $cachetime , которая определяет время жизни кеша.

Строки с 9 по 13 определяют условное выражение, которое служит для проверки наличия файла с именем, определенным в переменной $cachefile . Если файл существует, вставляется комментарий и файл, определенный в переменной $cachefile . Затем выражение exit прерывает выполнение скрипта и файл отправляется браузеру клиента. То есть, если найден статичный файл, то PHP код не будет выполняться сервером.

Строка 14 создает буфер, если файл, определенный переменной $cachefile не найден.

Шаг второй. Создаем файл bottom-cache.php

Теперь создаем второй файл PHP с именем bottom-cache.php и копируем в него следующий код:

Если файл с именем, определенным в переменной $cachefile отсутствует на сервере, выполняется данный код и создается файл. При следующем обращении к странице статичный $cachefile будет обслуживать браузер клиента вместо выполнения всего кода скрипта PHP.

Шаг три. Включаем файлы кеширования в код страницы

Теперь у нас есть два необходимых файла. Просто включаем их в страницу PHP, которую нужно кешировать. Файл top-cache.php нужно включить в начало страницы, а файл bottom-cache.php - в конце:

Теперь, если проверить кеширование на медленных страницах, то можно убедиться, насколько они стали быстрее загружаться.

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

Сами кэши бываю двух видов — локальные и общие. Локальный это кеш, хранимый непосредственно на диске у клиента, создаваемый и управляемый его браузером. Общий — кэш прокси-сервера организации или провайдера и может состоять из одного или нескольких прокси-серверов. Локальный кеш присутствует, наверное в каждом браузере, общими пользуется значительная часть людей использующих Internet. И если малую часть сайтов сейчас оценивают по расходу трафика, то скорость загрузки — важный критерий, который должен учитываться при разработке Вашего web-проекта.
Для динамических страниц, создаваемых в результате работы PHP-программы, казалось бы, кэширование вредно. Содержание страницы формируются по запросу пользователя на основе какого-либо источника данных. Однако, кэширование может быть полезным. Управляя им Вы можете сделать работу с Вашим сервером комфортнее для пользователя, разрешая загрузку из кэш определенных страниц, предотвращая тем самым их повторную выгрузку с Вашего сервера и экономя пользователю время и трафик.

Кэшировать или нет?

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

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

Однако, если мы говорим о том же графике но за вчерашний день, то кэширование рекомендуется, так как данные изменяться уже не будут и мы можем экономить себе и пользователю ресурсы и время на загрузку таких страниц помещением их в локальный или общий кэш. Как продолжение этой ситуации формирование графика не в реальном масштабе времени, а ежечасно. Тут Вы можете заранее предсказать дату окончания «срока годности» сформированных данных.

Общие принципы сохранения страниц в кэш

PHP-программа может управлять кэшированием результатов ее работы формируя дополнительные поля в заголовке HTTP ответа вызовом функции Header().
Несколько общих утверждений характерных не только для PHP-программ:

  • Страницы передаваемые по POST никогда не сохраняются в кэш.
  • Страницы запрашиваемые по GET и содержащие параметры (в URL присутствует ‘?’) не сохраняются в кэш, если не указано обратное.

Таким образом в большинстве ситуаций дополнительных инструкций в программу добавлять не надо. Основные моменты на которые следует обратить внимание можно свести к двум:

  • запрет кэширования документов, кэшируемых по умолчанию
  • кэширование документов, не подлежащих кэшированию по умолчанию.

Запрет кэширования документов, кэшируемых по умолчанию

Эта задача возникает для PHP-скриптов вызываемых без параметров или являющимися индексами директорий, однако формирующих данные персонально под пользователя (например на основе cookies или user agent) или работающих на основе быстро изменяющихся данных. По спецификации HTTP/1.1 мы можем управлять следующими полями:

Expires
Задает дату истечения срока годности документа. Задание ее в прошлом определяет запрет кэш для данной страницы.

Cache-control: no-cache
Управление кэш. Значение no-cache определяет запрет кэш данной страницы. Для версии протокола HTTP/1.0 действует «Pragma: no-cache».

Last-Modified
Дата послднего изменения содержимого. Поле актуально только для статических страниц. Apache заменяет это поле значением поля Date для динамически генерируемых страниц, в том числе для страниц содержащих SSI.

На сайте www.php.net дается следующий код для запрета кеширования.

header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date in the past
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); // always modified
header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
header("Pragma: no-cache"); // HTTP/1.0

Однако, данный заголовок избыточен. В большинстве случаев достаточно:

Чтобы пометить документ как «уже устаревший» следует установить Expires равным полю Date .
header("Expires: " . gmdate("D, d M Y H:i:s") . " GMT");

Ну и не следует забывать, что формы, запрошенные по POST также не подлежат кэшированию.

Кэширование документов, не подлежащих кэшированию по умолчанию

Обратная задача, может показаться на первый взгляд абсурдной. Однако и в этом существует потребность. Кроме простой минимизации трафика при разработке web-программы следует учитывать комфортность работы с ней пользователя. Например, некоторые страницы Вашего сервера формируются на основе статических данных большого объема. Возможность включения их в кэш существенно улучшит скорость работы сервера для пользователя и частично освободит Ваш от многочисленных повторных генераций такой страницы. Заголовок разрешающий сохранение на прокси-серверах:

Статья по теме: Поисковое продвижение интернет-магазина в Яндексе и Google: чек-лист аудита факторов ранжирования


Если страница учитывает информацию сохраненную в браузере пользователя (тип и версию браузера, ключи, авторизацию и т. д.) такую страницу нельзя сохранить на прокси, однако возможно ее сохранение в локальном кэш браузера:

header("Cache-control: private");

Кэширование до истечения корректности

Описанные выше решения довольно прямолинейны, хотя и подходят для большинства задач. Но протокол HTTP/1.1 имеет средства для более тонкого управления кэш страниц, и существуют задачи требующие применения этих механизмов. Как пример — веб-приложения, работающие с данными большого объема и прогнозируемой динамичностью. Корректность данных может устанавливаться как по дате прогнозируемого обновления, так и по изменению содержания. Для этих случаев используются разные заголовки управления кэшем.

Кэширование с прогнозируемым обновлением

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

$dt_tmp=getdate(date("U"));
header("Expires: " . gmdate("D, d M Y H:i:s", date("U")-(86400*($dt_tmp["wday"]-8))) . " GMT");
header("Cache-control: public");

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

Другой подход, применяемый при более оперативном обновлении информации и одновременной высокой посещаемости сервера (иначе кэширование не будет эффективным) состоит в использовании заголовка Cache-control: max-age=секунды, определяющий время по истечении которого документ считается устаревшим и имеющий больший приоритет при вычислении «свежести» документа.

Кэширования промежуточных данных является наиболее примитивным и в то же время самым полезным способом оптимизации, где вам не нужно возиться с какими-либо сложными алгоритмами или предварительной оптимизацией. Если вы собираетесь работать с PHP, то должны быть в курсе о проверенных методах и необходимых для этого инструментах, чтобы ваш сайт летал.

Статические локальные переменные

Быстрым и эффективным способом кэширования результатов функции/метода является использование статических локальных переменных. Рассмотрим пример, который выполняет сложные вычисления:

Теперь при каждом использовании функции в коде, независимо от количества вызовов, функция будет выполнять вычисления только один раз.

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

Статические переменные не разделяются между процессами PHP и могут кэшировать только на короткое время – охватывающее время выполнения сценария. Хорошим кандидатом является метод, который много раз вызывается из нескольких мест, например, для хранения состояния пользователя или результата, требующего большого объёма математических расчетов.

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

Функции разделяемой памяти APC

PHP является полу-компилируемым языком, а это значит, что каждый сценарий компилируется непосредственно не в машинный код, а в промежуточный код, известный как набор опкодов(байт-код). Данный шаг компиляции потребляет много ресурсов процессора и должен выполняться каждый раз при выполнении сценария. APC (Alternative PHP Cache) это расширение, которое пропускает этот шаг компиляции за счет кэширования опкодов в памяти.

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

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

Чтобы сделать кэш недействительным, можно использовать значения продолжительности существования (TTL), как в примере выше, или следующие функции:

Другие примечания по поддержке разделяемой памяти в APC:

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

С точки зрения производительности - статические переменные всегда будет быстрее, чем функции apc_fetch/apc_store , поскольку доступ к разделяемой памяти должен быть заблокирован и синхронизирован, чтобы предотвратить конфликтные ситуации.

APC является довольно популярным расширением, и поддерживается основными разработчиками PHP и (весьма вероятно) будет поставляться в комплекте с PHP 5.4.

Memcached для больших распределенных кэшей

Как только сайт начинает получать много посещений, в конечном счете, появляется задача распределения нагрузки между различными машинами. В результате этого обычно требуется переместить PHP на несколько серверов приложений. Если вы использовали APC кэширование раньше - каждый сервер приложений теперь имеет отдельный и дублирующий кэш.

Memcached с другой стороны, представляет собой распределенную службу для хранения данных ключ-значение. Расширение может быть развернуто на отдельном выделенном сервере или в том же стеке PHP приложений. Важно понимать, что нет никакой синхронизации/репликации между несколькими Memcached серверами, и они совсем ничего не знают друг о друге. Фактический сервер, который будет использоваться для хранения, выбирается на стороне клиента с помощью алгоритма хеширования на основе предоставленных данных "ключа". Именно поэтому, в отличие от APC, кэшированные данные не дублируются между различными машинами, а память лучше используется для крупных распределенных приложений.

API очень похож на функциональность разделяемой памяти в APC. Тот же пример с обменом валют, реализованный с помощью PHP расширения Memcache:

pconnect("..."); function getCurrencyRates() { global $memcache; $rates = $memcache->get("rates"); if ($rates === false) { // Запрос к базе данных или внешним API функциям $rates = ...; // Курс обмена будет кэширован на 1 час $memcache->set("rates", $rates, 0, 3600); } return $rates; } ?>

Обновление кэша такое же, как и в APC – с использованием TTL функциональности или набора функций.

delete("rates"); // Удаляет кэш $rates $memcache->flush(); // Удаляет все кэшированные данные?>

Локальный APC кэш всегда будет более быстрым методом по сравнению с Memcached.

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

Таблицы баз данных в оперативной памяти

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

MySQL предоставляет таблицы в оперативной памяти в подсистеме хранения данных MEMORY. Хотя данные будут очищены после перезагрузки сервера - схемы таблиц будут сохраняться:

CREATE TABLE test (...) ENGINE = MEMORY

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

CREATE TEMPORARY TABLE test (...)

В SQLite можно создать в памяти целую базу данных, но с теми же ограничениями, как и в PostgreSQL - данные будут существовать только на время сессии, и понадобится использовать постоянные соединения, чтобы поддерживать их между несколькими запросами.

true)); ?>

Так что же можно сделать с таблицей в оперативной памяти? Хотя такая таблица никогда не будет быстрее доступа к данным ключ-значение в APC/Memcached, вы получаете мощь SQL. Кэшированные данные могут быть отфильтрованы, упорядочены, сгруппированы и даже объединены с другими данными в таблицах.

Простой файловый кэш

Кэш на основе плоских файлов должен быть альтернативой упомянутым выше методам и использоваться только когда система не имеет необходимых расширений или данные, о которых идет речь, не могут храниться в памяти (например, из-за размера)

Поскольку кэширование выполняется для повышения производительности, а оптимизация производительности является результатом высокого параллелизма - всегда следует использовать блокировку файлов, чтобы предотвратить состояние гонки при чтении/записи:



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: