На информационном ресурсе применяются рекомендательные технологии (информационные технологии предоставления информации на основе сбора, систематизации и анализа сведений, относящихся к предпочтениям пользователей сети "Интернет", находящихся на территории Российской Федерации)

GeekBrains

4 подписчика

Ускоряем PHP-проект с помощью кэширования

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

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

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

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

Какие бывают подходы?

Существует множество подходов к кэшированию. Список совместимых с PHP инструментов можно посмотреть на странице PHP-cache. Самые распространенные из них:

  • Apcu
  • Array
  • Memcached
  • Redis

Давайте разберемся, какие особенности есть у каждого из них и чем они отличаются друг от друга.

APCu

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

 php -i | grep 'apc.enabled' # Ожидаем увидеть: # apc.enabled => On => On

Другой способ проверки: создайте файл index.php и поместите в него вызов функции phpinfo(). Убедитесь, что у вас настроен веб-сервер для используемой директории и откройте скрипт в браузере через адрес сервера. Нас интересует секция APCu: если внутри неё есть пункт APCu Support: Enabled, значит всё хорошо, мы можем идти дальше.

Если APCu у вас не установлен,  сделать это можно следующим способом:

  1. Запустите окно терминала (Linux/MacOS) или командную строку (Windows. Введите в поиске "cmd").
  2. Выполните команду:
 pecl install apcu apcu_bc
  1. Откройте в любом текстовом редакторе файл конфигурации php.ini и убедитесь в наличии следующих строк:

 # Windows extension=php_apcu.dll extension=php_apcu_bc.dll   apc.enabled=1 apc.enable_cli=1   #Linux / MacOS extension="apcu.so" extension="apc.so"   apc.enabled=1 apc.enable_cli=1
  1. Если указанных строк нет, добавьте их и сохраните файл конфигурации.
  2. Повторите проверку наличия установленного APCu.

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

 $cacheKey = 'product_1'; $ttl = 600; // 10 минут.   // Проверка доступности APCu $isEnabled = apcu_enabled();   // Проверяет, есть ли данные в кэше по ключу $isExisted = apcu_exists($cacheKey);   // Сохраняет данные в кэш. В случае успеха возвращает true // Аргумент $ttl определяет, как долго будет храниться кэш (секунды) $isStored = apcu_store($cacheKey, ['name' => 'Demo product'], $ttl);   // Получает данные из кэша по ключу. В случае их отсутствия, вернет false $data = apcu_fetch($cacheKey);   // Удаляет данные из кэша по ключу $isDeleted = apcu_delete($cacheKey);   var_dump([     'is_enabled'   => $isEnabled,     'is_existed'   => $isExisted,     'is_stored'    => $isStored,     'is_deleted'   => $isDeleted,     'fetched_data' => $data, ]);

Любой кэш работает по принципу key-value хранилища: это значит, что данные сохраняются со специальным ключом, по которому и происходит обращение. В данном случае ключ хранится в переменной $cacheKey.

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

Array-кэш

Более простой, но не всегда применимый метод кэширования. Если APCu сохраняет данные и делает их доступными для последующих выполнений всеми процессами, то Array-кэш хранит их только в рамках обрабатываемого запроса.

Что это значит? Представим, что у вас есть страница с комментариями пользователей. Один пользователь может оставить несколько сообщений, и когда мы будем собирать массив этих данных, нам не захочется несколько раз  ходить в базу данных за одним и тем же пользователем. Что мы можем сделать, так это сохранить полученные данные в массив, чтобы при его наличии не делать повторный запрос.

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

 class CustomArrayCache {     /**      * Массив приватный и статический      * - приватный — чтобы обращаться к нему можно было только      * из методов класса.      * - статический — чтобы свойство было доступно во всех экземплярах      */     private static array $memory = [];       // Метод сохранения данных в памяти     public function store(string $key, $value): bool     {         self::$memory[$key] = $value;           return true;     }       // Метод получения данных из памяти     public function fetch(string $key)     {         return self::$memory[$key] ?? null;     }       // Метод удаления данных из памяти     public function delete(string $key): bool     {         unset(self::$memory[$key]);           return true;     }       // Метод проверки наличия данных по ключу     public function exists(string $key): bool     {         return array_key_exists($key, self::$memory);     } }

Из-за  своей ограниченности этот подход применяется редко, однако знать о нём полезно.

Memcached и Redis

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

  • изолированность от PHP: за кэш отвечают отдельные сервисы;
  • возможность кластеризации: если нагрузка на ваш проект очень велика, кластеризация сервисов кэширования поможет с ней справиться.

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

Стандарт PSR-16

В PSR есть два стандарта, посвящённых кэшированию: PSR-6 (обычный интерфейс кэширования) и PSR-16 (простой интерфейс кэширования) — мы сосредоточимся на PSR-16.

Этот стандарт предлагает специальный интерфейс (CacheInterface), которому могут удовлетворять классы, выполняющие функцию кэширования. Согласно ему, такие классы должны реализовывать следующие методы:

  • get($key, $default) — получение данных из кэша: вторым аргументом передаётся значение, которое будет возвращено в случае отсутствия этих данных;
  • set($key, $value, $ttl = null) — сохранение данных в кэш: как мы уже видели ранее, третьим параметром передаётся время хранения в секундах. Если оставить его пустым (null), значение будет подставлено по умолчанию из конфигурации кэша;
  • delete($key) — удаляет данные по ключу;
  • clear() — очищает все хранилище;
  • getMultiple($keys, $default) — позволяет получить данные сразу по нескольким ключам;
  • setMultiple($values, $ttl = null) — позволяет записать сразу несколько значений. В качестве $value мы передаем ассоциативный массив, где ключ — $key для кэша, а значение — данные для сохранения;
  • deleteMultiple($keys) — удаляет данные по нескольким ключам;
  • has($key) — проверяет наличие данных по ключу.

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

Главные преимущества соблюдения стандартов PSR заключаются в том, что

  • они поддерживаются большинством популярных библиотек;
  • многие PHP-программисты придерживаются PSR и с легкостью освоятся в вашем коде;
  • благодаря интерфейсу, мы можем легко подменять используемый сервис на любой другой, поддерживающий PSR-16.

Давайте подробнее рассмотрим последний пункт и его преимущества.

Подключение PSR-16 библиотек

Библиотеки, создающие «обертку» над существующими инструментами кэширования для соответствия интерфейсу называются адаптерами. Для примера, рассмотрим адаптеры тех методов, что мы уже обсудили:

Все они удовлетворяют PSR-16 и поэтому применяются одинаково, однако логика «под капотом» у каждого своя.

Для примера давайте загрузим APCu- и Array-адаптеры в наш проект с помощью Composer.

 composer require cache/array-adapter composer require cache/apcu-adapter # Или composer req cache/apcu-adapter cache/array-adapter

Давайте представим, что у нас есть специальный класс для получения продуктов из базы данных. Назовем его ProductRepository, у него есть метод find($id), который возвращает продукт по его идентификатору, а если такого продукта нет — null.

 class ProductRepository {     /**      * Чтобы не усложнять пример, обусловимся, что в качестве продукта      * возвращается массив, а если его нет — null      */     public function find(int $id): ?array     {         // ...         // Получаем данные из БД         return $someProduct;     } }

Если мы хотим подключить кэширование, мы не должны делать это внутри репозитория, потому что его ответственность — возвращать данные из базы данных. Куда же мы тогда добавим кэш? Есть несколько популярных решений, самое простое — дополнительный класс-провайдер. Всё, что он будет делать — пробовать получить данные из кэша, а если это не получится — обратится в репозиторий. Для этого в конструкторе такого класса определим две зависимости — наш репозиторий и CacheInterface. Почему именно интерфейс? Потому что так мы сможем использовать абсолютно любой из упомянутых адаптеров или других классов, удовлетворяющих PSR-16.

 class ProductDataProvider {    private ProductRepository $productRepository;    private CacheInterface $cache;      public function __construct(ProductRepository $productRepository, CacheInterface $cache)    {        $this->productRepository = $productRepository;        $this->cache             = $cache;    }      public function get(int $productId): ?array    {        $cacheKey = sprintf('product_%d', $productId);          // Пробуем получить продукт из кэша        $product = $this->cache->get($cacheKey);        if ($product !== null) {            // Если продукт есть, возвращаем            // Временно выведем echo, чтобы понять, что данные из кэша            echo 'Данные из кэша' . PHP_EOL; // PHP_EOL - перенос строки            return $product;        }        // Если продукта нет, получаем его из репозитория        $product = $this->productRepository->find($productId);          if ($product !== null) {            // Теперь сохраним полученный продукт в кэш для будущих запросов            // Также временно выведем echo            echo 'Данные из БД' . PHP_EOL;            $this->cache->set($cacheKey, $product);        }          return $product;    } }

Наш класс готов. Теперь давайте рассмотрим его применение в сочетании с APCu-адаптером.

 use Cache\\Adapter\\Apcu\\ApcuCachePool;   // Подключаем автозагрузчик Composer require_once 'vendor/autoload.php';   // Наш репозиторий $productRepository = new ProductRepository(); // APCu-кэш адаптер. Не требует никаких дополнительных настроек $cache = new ApcuCachePool();   // Создаем провайдер, передаем зависимости $productDataProvider = new ProductDataProvider(     $productRepository,     $cache );   // Если в БД есть такой продукт, он к нам вернется $product = $productDataProvider->get(1); var_dump($product);

Если же мы захотим, заменить APCu-кэширование на Array-адаптер или любой другой, мы просто передадим новый подход в провайдер вместо старого, потому что все они реализуют CacheInterface.

 use Cache\\Adapter\\PHPArray\\ArrayCachePool; // ... $productRepository = new ProductRepository(); //$cache = new ApcuCachePool(); $cache = new ArrayCachePool(); $productDataProvider = new ProductDataProvider(     $productRepository,     $cache ); // ...

Состояние гонки и обновление данных

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

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

  • пользователь 1 получил сущность из кэша;
  • пользователь 1 обновил сущность в БД;
  • пользователь 2 получил сущность из кэша;
  • пользователь 1 обновил данные в кэше;
  • пользователь 2 обновил сущность в БД, но перезаписал её старыми данными, потому что сущность была неактуальна на момент получения и т. д.

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

Когда вы получаете любую сущность в коде с целью её обновления, всегда используйте данные из БД.

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

Вы можете либо обращаться в нужных местах к ProductRepository вместо ProductDataProvider, либо добавить аргумент к методу DataProvider. Например, такой ($fromCache):

 class ProductDataProvider {     // ...     public function get(int $productId, bool $fromCache = true): ?array     {         $cacheKey = sprintf('product_%d', $productId);           $product = $fromCache ? $this->cache->get($cacheKey) : null;         if ($product !== null) {             return $product;         }         $product = $this->productRepository->find($productId);           if ($product !== null) {             $this->cache->set($cacheKey, $product);         }           return $product;     } }

Заключение

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

Однако вне зависимости от того, будете ли вы применять эти подходы в ваших текущих проектах или нет, стоит изучить их и применить на практике, потому что этот навык обязательно пригодится вам в работе в крупных командах.

Подводя итог, повторим ключевые идеи статьи:

  • Соблюдение PSR-16 (или PSR-6) позволит вам с легкостью подключить для кэширования стороннюю библиотеку и сделает ваш код понятным другим разработчикам.
  • Для небольших проектов хорошим решением для кэширования станет APCu, т. к. он прост в настройке и использует оперативную память, доступ к которой очень высокий.
  • Для всех совместимых с PHP-инструментов кэширования есть адаптеры, которые можно посмотреть на сайте php-cache.com.
  • Кэширование — отдельная ответственность. Старайтесь реализовывать работу с кэшем в отдельных классах.
  • Если мы собираемся обновить сущность, её следует получать из БД. Если сущность нужна нам только для просмотра — мы можем запросить её из кэша.
  • В крупных проектах для получения возможности масштабирования применяются Memcached или Redis.

 

Ссылка на первоисточник
Рекомендуем
Популярное
наверх