Kuzma's PHP LookHome page: http://kuzma.russofile.ru |
|
| Оригинал: Антология PHP, 2 том, глава 5 – Кэширование Автор: Harry Fuecks Перевод: Муллин Сергей (SiMM), Кузьма Феськов (kuzma@russofile.ru, http://kuzma.russofile.ru) Кэширование в PHPВведениеВ старые добрые времена, когда создание web-сайтов представляло из себя такое простое занятие, как набор нескольких HTML-страниц, отправка web-страниц в браузер была простой отправкой файла web-сервером. Посетители сайта могли видеть эти небольшие, исключительно текстовые странички, почти мгновенно (если не считать пользователей медленных модемов). Как только страница была загружена, браузер кэширует её где-нибудь на локальном компьютере, чтобы в случае повторного запроса страницы, можно было взять его локальную версию из кэша, послав лишь короткий запрос, чтобы убедиться, что страница на сервере не была изменена. Запросы обрабатывались быстро и как можно эффективней, и все были счастливы (кроме использующих модемы 9600 бод). Появление динамических web-страниц изменило положение вещей в худшую сторону, эффективно «сломав» эту модель обслуживания web-страниц благодаря наличию двух проблем:
Обычно для маленьких PHP-приложений вполне можно игнорировать существование этих проблем, однако с увеличением сложности и повышением трафика Вашего сайта Вы можете столкнуться с проблемами. Тем не менее, обе эти проблемы могут быть решены, первая путём кэширования на стороне сервера, вторая путём управления кэшированием на стороне клиента из вашего приложения. Подход, который вы будете использовать для решения проблем, будет зависеть от вашей области применения, но в этой главе мы увидим, как вы можете решить обе проблемы используя PHP и некоторые классы библиотеки PEAR. Обратите внимание, что в главе «Кэширование» обсуждаются только решения, осуществляемые при помощи PHP. Они не должны быть перепутаны с решениями кэширования скриптов, работающими на основе оптимизации и кэширования откомпилированных PHP-скриптов. В эту группу можно включить Zend Accelerator, ionCube PHP Accelerator и Turck MMCache, последний является единственным на сегодняшний день акселератором готовым для использования в PHP, установленном под Windows. Как я предотвращаю кэширование страницы браузерами?Прежде чем мы рассмотрим методы клиентского и серверного кэширования, в первую очередь мы должны понять, как вообще предотвратить кэширование страниц web-браузером (и прокси-серверами). Основной способ достижения этого использует мета-тэги HTML:
Вставив прошедшую дату в мета-тэг Expires, вы сообщаете браузеру, что кэшированная копия странички всегда является устаревшей. Это значит, что браузер никогда не должен кэшировать страницу. Мета-тэг Pragma: no-cache довольно хорошо поддерживаемое соглашение, которому следует большинство web-браузеров. Обнаружив этот тэг, они обычно не кэшируют страницу (хотя никаких гарантий нет, это всего лишь соглашение). Это хорошо звучит, но есть две проблемы, связанные с использованием мета-тэгов:
Лучший подход состоит в том, чтобы использовать непосредственно протокол HTTP с помощью функции PHP header, эквивалентно приведённым выше двум мета-тэгам:
Мы можем пойти на один шаг вперёд, воспользовавшись заголовком Cache-Control совместимым с браузерами, поддерживающими HTTP 1.1:
Это гарантирует, что никакой web-браузер или промежуточный прокси-сервер не будет кэшировать страницу, таким образом посетители всегда получат самую последнюю версию контента. Фактически, первый заголовок должен быть самодостаточным, это лучший способ гарантировать, что страница не кэшируется. Заголовки Cache-Control и Pragma добавлены с целью «подстраховаться». Хотя они не работают во всех браузерах или прокси, они отловят некоторые случаи, в которых Expires не работает должным образом (например, если дата на компьютере клиента установлена неправильно). Конечно, полный отказ от кэширования обеспечивает нас проблемами, которые мы обсуждали в начале этой главы. Сейчас мы рассмотрим решение этих проблем. Internet Explorer и кэширование загрузки файловНаше обсуждение о PDF, приведённое в главе 3, Альтернативные типы контента, объяснило, что проблемы могут возникать, когда вы имеете дело с кэшированием и загрузкой файлов. Если при обслуживании загрузки файла PHP-скриптом используются такие заголовки, как например Content-Disposition: attachment, filename=myFile.pdf или Content-Disposition: inline, filename=myFile.pdf у вас будут проблемы с Internet Explorer’ом, если вы сообщите браузеру не кэшировать страницу. Internet Explorer оперирует загрузкой довольно необычным образом, выполняя два запроса к web-сайту. Первый запрос загружает файл и сохраняет его в кэше, пока не будет создан второй запрос (без сохранения отклика). Этот запрос вызывает процесс передачи файла конечному пользователю в соответствии с типом файла (например, запускает Acrobat Reader, если файл является PDF-документом). Это значит, что если вы отправили заголовки, запрещающие браузеру кэшировать страницу, Internet Explorer удалит файл между первым и вторым запросом, в результате чего конечный пользователь ничего не получит. Если файл, который вы отдаёте PHP-скриптом, не изменяется, одним из простейших решений будет убрать «запрещающие кэширование» заголовки из скрипта. Если загружаемый файл регулярно изменяется (т.е. вы хотите, чтобы браузер загружал новейшую версию), вы должны использовать заголовок Last-Modified который будет рассмотрен в этой главе позднее, и гарантировать, что время модификации между двумя последовательными запросами не изменяется. Вы должны сделать это таким образом, чтобы не повлиять на пользователей браузеров, правильно оперирующих загрузкой. Одним из решений в этом случае будет сохранение файла на вашем web-сервере и предоставление простой ссылку к нему, предоставив web-серверу сообщать за вас заголовки кэширования. Конечно, это решение не может быть приемлемым, если предполагается авторизованный доступ к файлу, это решение допускает непосредственную загрузку сохранённого файла. Как я могу захватить данные на стороне сервера для кэширования?Пришло время взглянуть на то, как мы можем уменьшить задержку при помощи кэширования вывода на стороне сервера. Общий подход начинает предоставлять страницу как обычно, выполняя запросы к базе данных и так далее на PHP. Тем не менее, перед отправкой результата в браузер, мы захватываем его и сохраняем готовую страницу, например, в файле. При следующем запросе, PHP-скрипт сначала проверяет наличие кэшированной версии страницы. Если она существует, скрипт отправляет в браузер версию из кэша, исключая таким образом задержку на повторное создание страницы. Несколько слов о кэшировании при помощи шаблоновШаблонные движки типа Smarty часто говорят о кэшировании шаблонов. Обычно эти движки предлагают встроенный механизм для сохранения откомпилированной версии шаблона (т.е. генерируют из шаблона PHP-исходник), что предохраняет нас от необходимости парсить шаблон каждый раз, когда запрашивается страница. Это не нужно путать с кэшированием вывода, которое имеет отношение к кэшированию предоставляемого HTML (или другого вывода), который посылает PHP в браузер. Вы можете успешно использовать оба типа кэширования одновременно на одном и том же сайте. Сейчас мы рассмотрим встроенный механизм кэширования на PHP, использующий буферизацию вывода, который может использоваться вами независимо от способа создания контента (с шаблонами или без шаблонов). Рассмотрим ситуацию в которой ваш скрипт отображает результат использую, к примеру, echo или print, чтобы выдать данные непосредственно в браузер. В таком случае вы можете использовать функции управления выводом PHP для хранения данных в буферной памяти, над которой ваш PHP-скрипт имеет и доступ, и контроль. Вот простой пример:
Сам буфер хранит вывод как строку. Так, в вышеприведённом скрипте мы начинаем буферизацию с ob_start и используем echo, чтобы вывести что-либо. Затем мы используем ob_get_contents, чтобы выбрать данные, помещённые в буфер оператором echo, и сохранить их в строке. Функция ob_end_clean останавливает буферизацию вывода и уничтожает его содержимое, как альтернативу можно использовать ob_end_flush, чтобы вывести содержимое буфера. Вышеописанный скрипт выведет:
Другими словами, мы захватили вывод первого echo, затем послали его браузеру после второго echo. Как видно из этого простого примера, буферизация вывода является очень мощным инструментом для формирования вашего сайта, она обеспечивает решение для кэширования, как мы скоро увидим, и является отличным способом скрыть ошибки от посетителей вашего сайта (смотрите главу 10, Обработка ошибок). Она также обеспечивает альтернативную возможность для переадресации браузера в ситуациях типа аутентификации пользователя. Заголовки HTTP и буферизация выводаБуферизация вывода может помочь решить наиболее общую проблему, связанную с функцией header, не говоря уже о session_start и set_cookie. Обычно, если вы вызываете любую из этих функций после того, как начался вывод страницы, вы получите противное сообщение об ошибке. При включенной буферизации вывода единственным типом вывода, избегающим буферизации, являются HTTP-заголовки. Используя ob_start в самом начале выполнения вашего приложения, вы можете посылать заголовки в любой понравившейся точке программы, не сталкиваясь с обычными ошибками. Затем, как только вы будете уверены, что больше выводить HTTP-заголовки не потребуется, вы можете сразу же вывести содержимое страницы из буфера. (прим. переводчика: следует заметить что подобное использование данной функции крайне неоправдано. В большинстве случаев необходимости в использовании буферизации вывода для избавления ошибок указанного типа просто не существует и всё с лёгкостью может быть исправлено правильным проектированием приложения) Использование буферизации вывода для кэширования на стороне сервераВы уже видели базовый пример буферизации вывода, теперь следующий шаг, в котором буфер сохраняется в файл:
Сначала вышеописанный скрипт проверяет наличие существования версии странички в кэше, и, если она имеется, скрипт читает и выводит её. В противном случае, он использует буферизацию вывода для создания версии страницы в кэше. Она сохраняется как файл, после использования ob_end_flush для отображения страницы пользователю.
Блочная буферизацияУпрощённый подход кэширует выводимый буфер как одну страницу. Однако этот подход лишает вас реальных возможностей, предоставляемых функциями управления выводом PHP, улучшающих производительность вашего сайта методом соответственно различающихся сроков жизни вашего контента. Вне всякого сомнения, некоторые части отправляемой вами посетителю страницы изменяются очень редко, например, такие как шапку, меню и нижний колонтитул. Однако другие части, типа таблиц, содержащих обсуждения в форуме, могут изменяться довольно часто. Буферизация вывода может использоваться к кэшированию разделов страницы в отдельных файлах, затем создавать из них страницу – решение, устраняющее необходимость повторных запросов к базе данных, циклов while и т.д. Вы можете назначать каждому блоку страницы дату истечения срока, после которой пересоздаётся кэш-файл, или кроме того, вы можете включить в ваше приложение механизм, который будет удалять кэш-файл каждый раз, когда сохранённый в нём контент изменён. Вот пример, демонстрирующий этот принцип:
Первые две определённые нами функции, writeCache и readCache, используются соответственно для создания кэш-файлов и проверки их существования. Функция writeCache получает данные для кэширования в первом аргументе, и имя файла, используемое при создании кэш-файла. Функция readCache получает имя кэш-файла в первом параметре, вместе со временем в секундах, после которого кэш-файл должен считаться устаревшим. Если она сочтёт кэш-файл допустимым, скрипт вернёт его содержимое, в противном случае он вернёт FALSE, чтобы показать, что-либо кэш-файла не существует, либо он устарел. В этом примере я использовал процедурный подход. Однако я не советую делать это на практике, поскольку это закончится очень грязным кодом (смотри последующие решения с лучшей альтернативой) и, вероятно, вызовет проблемы с блокировкой файла (например, что случится, когда кто-то обращается к кэшу в момент его обновления?). Давайте продолжим этот пример. После того, как запущена буферизация вывода, начинается обработка. Сначала скрипт вызывает readCache, чтобы узнать, существует ли файл 3_header.cache, он содержит шапку страницы – заголовок HTML и начало тела. Мы используем функцию date PHP чтобы вывести время, когда страница фактически была сгенерирована, таким образом вы увидите различные кэш-файлы в работе, когда страница будет отображена.
Что же случается когда кэш-файл не найден? Выводится некоторый контент и присваивается переменной при помощи ob_get_contents, после чего буфер очищается функцией ob_clean. Это позволяет нам перехватывать вывод по частям и сопоставлять их с индивидуальными кэш-файлами при помощи writeCache. Заголовок страницы теперь хранится как файл, который может быть использован без нашего вмешательства в пересборку страницы. Давайте вернёмся на секунду к началу условного оператора. Когда мы вызывали readCache, мы передали ей время жизни кэша в 604800 секунд (одна неделя), readCache использует время модификации кэш-файла, чтобы определить, является ли кэш-файл всё ещё допустимым. Для содержимого (тела) страницы мы по прежнему будем использовать тот же процесс. Однако на сей раз при вызове readCache мы будем использовать время жизни кэша в пять секунд, кэш-файл будет модифицироваться каждый раз, когда он «старше» 5 секунд:
Нижний колонтитул эффективно изменять так же, как заголовок. После этого буферизация вывода останавливается и отображается содержимое трёх переменных, содержащих данные страницы:
Конечный результат выглядит примерно так:
Заголовок и нижний колонтитул обновляются еженедельно, в время как тело модифицируется, когда оно старее 5 секунд. Блок-схема на рисунке 5.1 суммирует методологию блочной буферизации. ![]() Вложенные буферыВы можете вкладывать один буфер в другой фактически до бесконечности, просто вызвав ob_start неоднократно. Это может быть полезным, если у вас имеется множество операций, использующих буфер вывода, например, одни перехватывают сообщения PHP об ошибках, другие имеют дело с кэшированием. Вы должны удостовериться, что ob_end_flush или ob_end_clean вызываются каждый раз, когда используется ob_start. Как мне реализовать простую систему кэширования на стороне сервера?Теперь, когда мы понимаем идеи буферизации вывода, пришло время рассмотреть, как мы можем использовать этот процесс в действии таким образом, чтобы его было легко поддерживать. Чтобы сделать это, мы воспользуемся небольшой помощью от PEAR::CacheLite (версия 1.1 использовалась в приведённых здесь примерах). Как я уже говорил, в интересах удобства последующей поддержки кода и получения надежного кеширующего механизма, разумно будет возложить ответственность за кеш-логику на те классы, которым вы доверяете. Cache_Lite представляет собой мощную, но простую в использовании, библиотеку для кеширования, которая берет на себя такие задачи, как временная блокировка кеш-файлов, их создание и проверка, управление буфером вывода и непосредственное кеширование результа работы функции или метода класса. Основной же причиной выбора этой библиотеки является относительно простая интеграция Cache_Lite в уже существующее приложение, которая требует лишь незначительных изменений в коде. Cache_Lite состоит из трех основных классов. Первым является базовый класс Cache_Lite, который отвечает только за создание и чтение кеш-файлов и не занимается буферизацией вывода. Данный класс можно использовать в одиночку в тех случаях, когда нет необходиомсти использовать буферизацию вывода, как например при сохранении результата разбора шаблона PHP скриптом. Приведенные здесь примеры не используют класс Cache_Lite напрямую и демонстрируют применение остальных двух классов. Cache_Lite_Function используется для вызова функции или метода класса и последующего кеширования результатов работы. Это может оказаться полезным, например для кеширования результата запроса к MySQL. Класс Cache_Lite_Output использует PHP функции контроля за выводом для перехвата данных, сгенерированных скриптом, и сохранения их в кеш-файлах. Это позволяет выполнять те же задачи, что и предыдущее решение. Вот пример, который покажет вам, как вы могли бы использовать Cache_Lite, чтобы выполнить задачу, которую мы рассмотрели в предыдущей части. Чтобы рассмотреть любое применение Cache_Lite, мы должны его сначала настроить - создать массив параметров - который определит поведение класса. Ниже мы рассмотрим их подробнее, а пока обратите внимание на то, что ваш скрипт должен иметь права на чтение и запись в каталог cahceDir.
Для каждой части вывода вашего скрипта, которую вы хотите кэшировать, необходимо установить время жизни кэша в секундах. это время определяет, как долго нужно брать данные из кэше. По истечении этого времени, данные в файле будут обновлены. Далее, мы вызываем метод start(), доступный только в классе Cahce_Lite_Output, который включает буферизацию вывода. В метод мы передаем 2 параметра: первый - идентификатор файла с кэшем, второй - группа (тип кэша). Параметр "группа" позволяет объединять несколько шаблонов, это позволяет производить групповые действия, например, удалить все файлы кэша в группе. Как только вывод нужной нам части закончен, мы должны вызвать метод stop(). Этот метод остановит буферизацию и сохранит содержимое буфера в файл.
Кэширование основного тела вывода (body) и нижней части (footer) происходит аналогично кэшированию заголовка. Обратите внимание, мы снова выставляем время жизни кэша, для каждой следующей части.
|