Кэширования промежуточных данных является наиболее примитивным и в то же время самым полезным способом оптимизации, где вам не нужно возиться с какими-либо сложными алгоритмами или предварительной оптимизацией. Если вы собираетесь работать с PHP, то должны быть в курсе о проверенных методах и необходимых для этого инструментах, чтобы ваш сайт летал.
Статические локальные переменные
Быстрым и эффективным способом кэширования результатов функции/метода является использование статических локальных переменных. Рассмотрим пример, который выполняет сложные вычисления:
1 2 3 4 5 6 7 8 9 |
<?php function getValue() { static $value; if (is_null($value)) { $value = ...; // Вычисление } return $value; } ?> |
Теперь при каждом использовании функции в коде, независимо от количества вызовов, функция будет выполнять вычисления только один раз.
Эта методика кэширования не зависит от внешних расширений и поддерживается на уровне языка. Поскольку статические локальные переменные доступны только в пределах функции, в которой они определены, обновление кэша может быть проблематичным. Как правило, для этого необходимо передать какую-либо логическую переменную $use_cache или использовать вместо них статические переменные класса.
Статические переменные не разделяются между процессами PHP и могут кэшировать только на короткое время – охватывающее время выполнения сценария. Хорошим кандидатом является метод, который много раз вызывается из нескольких мест, например, для хранения состояния пользователя или результата, требующего большого объёма математических расчетов.
Такая же функциональность может быть реализована с использованием глобальных переменных, но это приведет к загрязнению глобального пространства имен, поэтому так делать не рекомендуется.
Функции разделяемой памяти APC
PHP является полу-компилируемым языком, а это значит, что каждый сценарий компилируется непосредственно не в машинный код, а в промежуточный код, известный как набор опкодов(байт-код). Данный шаг компиляции потребляет много ресурсов процессора и должен выполняться каждый раз при выполнении сценария. APC (Alternative PHP Cache) это расширение, которое пропускает этот шаг компиляции за счет кэширования опкодов в памяти.
Хотя основным назначением APC обычно считается функциональность кэширования опкодов, расширение также включает некоторые дополнительные функции доступа к памяти.
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php function getCurrencyRates() { $rates = apc_fetch('rates'); if (empty($rates)) { // Запрос к базе данных или внешним API функциям $rates = ...; // Курс обмена будет кэширован на 1 час apc_store('rates', $rates, 3600); } return $rates; } ?> |
Теперь преимущество такого подхода очевидно - это использование разделяемой памяти. Этот тип памяти является общим для различных процессов/потоков и, в отличие от статических переменных, данные, которые кэшируются таким способом, будут существовать между несколькими запросами.
Чтобы сделать кэш недействительным, можно использовать значения продолжительности существования (TTL), как в примере выше, или следующие функции:
1 2 3 4 |
<?php apc_delete('rates'); // Удаляет кэш $rates apc_clear_cache('user'); // Удаляет все переменные кэша для user ?> |
Другие примечания по поддержке разделяемой памяти в APC:
• В конфигурационном INI файле есть директива для ограничения размера кэша. С соответствующими значениями TTL это дает возможность задавать приоритет кэшированным данным, когда в случае достижения предела памяти истекшие/старые значения будут исключены из кэша.
• С точки зрения производительности - статические переменные всегда будет быстрее, чем функции apc_fetch/apc_store , поскольку доступ к разделяемой памяти должен быть заблокирован и синхронизирован, чтобы предотвратить конфликтные ситуации.
• APC является довольно популярным расширением, и поддерживается основными разработчиками PHP и (весьма вероятно) будет поставляться в комплекте с PHP 5.4.
Memcached для больших распределенных кэшей
Как только сайт начинает получать много посещений, в конечном счете, появляется задача распределения нагрузки между различными машинами. В результате этого обычно требуется переместить PHP на несколько серверов приложений. Если вы использовали APC кэширование раньше - каждый сервер приложений теперь имеет отдельный и дублирующий кэш.
Memcached с другой стороны, представляет собой распределенную службу для хранения данных ключ-значение. Расширение может быть развернуто на отдельном выделенном сервере или в том же стеке PHP приложений. Важно понимать, что нет никакой синхронизации/репликации между несколькими Memcached серверами, и они совсем ничего не знают друг о друге. Фактический сервер, который будет использоваться для хранения, выбирается на стороне клиента с помощью алгоритма хеширования на основе предоставленных данных "ключа". Именно поэтому, в отличие от APC, кэшированные данные не дублируются между различными машинами, а память лучше используется для крупных распределенных приложений.
API очень похож на функциональность разделяемой памяти в APC. Тот же пример с обменом валют, реализованный с помощью PHP расширения Memcache:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php // Предполагаем, что есть только одна служба memcached $memcache = new Memcache(); $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 функциональности или набора функций.
1 2 3 4 |
<?php $memcache->delete('rates'); // Удаляет кэш $rates $memcache->flush(); // Удаляет все кэшированные данные ?> |
Локальный APC кэш всегда будет более быстрым методом по сравнению с Memcached.
В сети всегда есть задержки, когда клиенту нужно обмениваться информацией с серверной службой через выделенный текстовый протокол.
Таблицы баз данных в оперативной памяти
Хотя это не относится непосредственно к PHP, многие системы управления базами данных имеют реализацию таблиц, находящихся в оперативной памяти. Данные, хранящиеся в таких таблицах, не сохраняются между перезапусками сервера, гарантированно содержатся в памяти и никогда на выгружаются на диск. Это означает более быстрый доступ к записям, что делает их пригодными для кэширования данных.
MySQL предоставляет таблицы в оперативной памяти в подсистеме хранения данных MEMORY. Хотя данные будут очищены после перезагрузки сервера - схемы таблиц будут сохраняться:
CREATE TABLE test (...) ENGINE = MEMORY
В PostgreSQL есть временные таблицы, которые существуют только во время сессии, чтобы кэшировать данные во временной таблице необходимо поддерживать постоянное соединение с базой данных.
CREATE TEMPORARY TABLE test (...)
В SQLite можно создать в памяти целую базу данных, но с теми же ограничениями, как и в PostgreSQL - данные будут существовать только на время сессии, и понадобится использовать постоянные соединения, чтобы поддерживать их между несколькими запросами.
1 2 3 4 5 6 7 8 |
<?php $pdo = new PDO( 'sqlite::memory:', null, null, array(PDO::ATTR_PERSISTENT => true) ); ?> |
Так что же можно сделать с таблицей в оперативной памяти? Хотя такая таблица никогда не будет быстрее доступа к данным ключ-значение в APC/Memcached, вы получаете мощь SQL. Кэшированные данные могут быть отфильтрованы, упорядочены, сгруппированы и даже объединены с другими данными в таблицах.
Простой файловый кэш
Кэш на основе плоских файлов должен быть альтернативой упомянутым выше методам и использоваться только когда система не имеет необходимых расширений или данные, о которых идет речь, не могут храниться в памяти (например, из-за размера)
Поскольку кэширование выполняется для повышения производительности, а оптимизация производительности является результатом высокого параллелизма - всегда следует использовать блокировку файлов, чтобы предотвратить состояние гонки при чтении/записи:
1 2 3 4 5 6 7 8 9 10 11 |
<?php // Устанавливаем монопольную блокировку при записи file_put_contents('...', $data, LOCK_EX); // Используем разделяемую блокировку для чтения $fp = fopen('...', 'r'); flock($fp, LOCK_SH); $data = stream_get_contents($fp); flock($fp, LOCK_UN); // Снимаем блокировку fclose($fp); ?> |