ITDumka
Кеширование в PHP - теперь немного лучше, чем просто кеш

В этой короткой статье я расскажу, как я убирал тривиальность из политики кеширования на файлах в PHP сценариях. В принципе это всё можно применить и не к "файловому" кешированию. Надеюсь, многим эта статейка принесет пользу.


Итак, что же мне не нравилось в моей тривиальности, и что я хотел бы улучшить?

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

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

Сначала я проверял актуальность кешированных данных и если они таковыми не являлись, то проводил операцию получения данных и записывал "свежий" кеш на диск. Иначе - если кеш был актуален - просто брал данные от туда. Вот пример из моего старенького шаблонизатора:


Listing №1 (PHP)
  1. <?php
  2. if ($this->_Cache->IsActual()) {
  3.   return $this->_Cache->Read();
  4. }
  5. else {
  6.   $Content = $this->Parse($File);
  7.   $this->_Cache->Write($Content);
  8.   return $Content;
  9. }
  10. ?>


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


Listing №2 (PHP)
  1. <?php
  2. public function IsActual()
  3. {
  4.   clearstatcache();
  5.   if (!file_exists($this->_Id)) {
  6.     return FALSE;
  7.   }
  8.   if (!($CreateTime = filemtime($this->_Id))) {
  9.     return FALSE;
  10.   }
  11.   if (($CreateTime + $this->_Expired) < time()) {
  12.     @unlink($this->_Id);
  13.     return FALSE;
  14.   }
  15.   return TRUE;
  16. }
  17. ?>


И вот, в один прекрасный день я решил попытаться избавиться от таких проблем:

  1. Одновременное обновление большого количества кешированных данных. То есть при практически одновременном обращении пользователей в момент устаревания кеша, происходит не только большая нагрузка на сервер но и конфликты кеширования. Большая нагрузка происходит от того, что кеш не может быстро обновиться, так как данных довольно много. Поэтому каждый пользователь фактически заново запускает процедуру обновления кеша, что влечет за собой конфликты, которые в свою очередь оказывают еще большую нагрузку на сервер
  2. Медленное обновление кеша, вследствие его немалого объема. То есть необходимо, большой кеш обновить как можно быстрее

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

  1. Разбиваем большой кешь на отдельные куски
  2. Заранее готовим каждый из кусков в разные моменты времени, маскируя наличие нового кеша
  3. Заменяем старый кеш на новый быстрым методом

Вот и всё - очень просто и со вкусом.

Теперь, по традиции, непосредственно сама реализация. Она далека от идеала и предназначена всего лишь для объяснения сути. Но все же давайте раздуем примерчик интерфейсом для backend-а. А также, в связи с тем, что я все данные получаю из функций и методов, я "приноровлю" механизм кеширования к методам получения возвращаемых результатов.

Для начала сконструируем интерфейс с нужными функциями:


Listing №3 (PHP)
  1. <?php
  2. interface Cachebackend {
  3.   /**
  4.    * @param string $Key
  5.    */
  6.   public function GetCache($Key);
  7.   /**
  8.    * @param string $Key
  9.    * @param int    $ExpiredPeriod
  10.    */
  11.   public function IsActual($Key, $ExpiredPeriod);
  12.   /**
  13.    * @param string $Key
  14.    * @param int    $ExpiredPeriod
  15.    * @param int    $SoonPeriod
  16.    * @param string $SoonFuseKeyPostfix
  17.    */
  18.   public function IsNeedCreateNewCache($Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix);
  19.   /**
  20.    * @param string $Key
  21.    * @param string $Data
  22.    */
  23.   public function PutCache($Key, $Data);
  24.   /**
  25.    * @param stirng $PreparedKey
  26.    * @param string $RenameKey
  27.    * @return bool
  28.    */
  29.   public function Rename($PreparedKey, $RenameKey);
  30. }
  31. ?>


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


Listing №4 (PHP)
  1. <?php
  2. class Cacher {
  3.   /**
  4.    * @var Cachebackend
  5.    */
  6.   private static $_Backend = NULL;
  7.   /**
  8.    * @var mixed
  9.    */
  10.   private static $_CallbackSignature = array();
  11.   /**
  12.    * @var array
  13.    */
  14.   private static $_CallbackArguments = array();
  15.   /**
  16.    * @static
  17.    * @param string $Tag
  18.    * @param string $Key
  19.    * @param int    $ExpiredPeriod
  20.    * @param int    $SoonPeriod
  21.    * @param sting  $SoonFuseKeyPostfix '.next'
  22.    * @return mixed
  23.    */
  24.   public static function GetData($Tag, $Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix = '.next')
  25.   {
  26.     $Key = CACHE_PATH . strtolower($Tag) . '_' . strtolower($Key) . CACHE_EXT;
  27.     if (NULL === self::$_Backend) {
  28.       self::$_Backend = new Cachefilebackend;
  29.     }
  30.     if (self::$_Backend->IsActual($Key, $ExpiredPeriod)) {
  31.       $Data = self::$_Backend->GetCache($Key);
  32.       if(self::$_Backend->IsNeedCreateNewCache($Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix)) {
  33.         $CallbackData = self::_GetCallbackResult();
  34.         self::$_Backend->PutCache($Key . $SoonFuseKeyPostfix, $CallbackData);
  35.       }
  36.       return $Data;
  37.     }
  38.     else {
  39.       if(self::$_Backend->IsActual($Key . $SoonFuseKeyPostfix, $ExpiredPeriod)) {
  40.         $Data = self::$_Backend->GetCache($Key . $SoonFuseKeyPostfix);
  41.         self::$_Backend->Rename($Key . $SoonFuseKeyPostfix, $Key);
  42.         return $Data;
  43.       }
  44.       else {
  45.         $CallbackData = self::_GetCallbackResult();
  46.         self::$_Backend->PutCache($Key, $CallbackData);
  47.         return $CallbackData;
  48.       }
  49.     }
  50.   }
  51.   /**
  52.    * @static
  53.    * @param string $CallbackFunction
  54.    * @param array $CallbackArguments
  55.    * @param mixed $CallbackObject NULL
  56.    */
  57.   public static function SetCallback($CallbackFunction, $CallbackArguments, $CallbackObject = NULL)
  58.   {
  59.     if ($CallbackObject) {
  60.       self::$_CallbackSignature = array($CallbackObject, $CallbackFunction);
  61.     }
  62.     else {
  63.       self::$_CallbackSignature = $CallbackFunction;
  64.     }
  65.     self::$_CallbackArguments = $CallbackArguments;
  66.   }
  67.   /**
  68.    * @static
  69.    * @return mixed
  70.    */
  71.   private static function _GetCallbackResult()
  72.   {
  73.     self::_CheckCallback();
  74.     return call_user_func_array(self::$_CallbackSignature, self::$_CallbackArguments);
  75.   }
  76.   /**
  77.    * @static
  78.    */
  79.   private static function _CheckCallback()
  80.   {
  81.     if(!is_callable(self::$_CallbackSignature, FALSE, $CallableName)) {
  82.       throw new Exception($CallableName . ' is not correct callback');
  83.     }
  84.     if(!is_array(self::$_CallbackArguments)) {
  85.       throw new Exception('Callback arguments must be an array');
  86.     }
  87.   }
  88. }
  89. ?>


Давайте немного разберемся, что к чему.

Итак, основной метод - это метод GetData. Что мы передаем ему:

  1. $Tag - идентификатор группы кеш-данных. Предназначен для управления целой группой кеш-данных.
  2. $Key - идентификатор конкретных кеш-данных. Однозначно идентифицирует кешируемые данные.
  3. $ExpiredPeriod - время жизни кеша. Это время можно даже назвать как время "псевдо" жизни, ведь новый кеш появится до истечения этого времени, хотя старый и удалиться именно по этому значению.
  4. $SoonPeriod - период времени, который определяет временной интервал между появлением нового кеша и удалением старого: T(удаления старого кеша) - T(создания нового кеша) = $SoonPeriod
  5. $SoonFuseKeyPostfix - параметр по умолчанию, обозначающий расширение файлов нового кеша, чтобы не путать их с еще существующими файлами старого кеша.

Константы CACHE_PATH и CACHE_EXT определяют кеш-директорию и расширение кеш-файлов соответственно. Метод GetData создает объект backend-а и выполняет соответствующие действия по управлению кешем:

  1. Проверяет актуальности кеша и если он таков, то отдает его данные
  2. Создает новый кешь, если время для необходимости его создания наступило
  3. Если кешированные данные не актуальны и новый кешь создан, то происходит замена старого кеша на новый и отдача соответствующих данных из нового кеша
  4. Если ни один из кешей не актуален то выполняется естественное получение данных, а также запись их в кеш

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

Функция SetCallback может принимать как метод объекта, так и функцию для получения кешируемых данных. Остальные две функции выполняют внутреннюю работу.

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


Listing №5 (PHP)
  1. <?php
  2. class Cachefilebackend implements Cachebackend {
  3.   /**
  4.    * @see Cachebackend
  5.    */
  6.   public function GetCache($Key)
  7.   {
  8.     clearstatcache();
  9.     if (file_exists($Key)) {
  10.       $Content = '';
  11.       $Content = @file_get_contents($Key);
  12.       return unserialize((string)$Content);
  13.     }
  14.     else {
  15.       throw new Exception('Cached keyfile "' . $Key. '" does not exists');
  16.     }
  17.   }
  18.   /**
  19.    * @see Cachebackend
  20.    */
  21.   public function IsActual($Key, $ExpiredPeriod)
  22.   {
  23.     clearstatcache();
  24.     if (file_exists($Key)) {
  25.       if (time() - filemtime($Key) > $ExpiredPeriod) {
  26.         @unlink($Key);
  27.         return FALSE;
  28.       }
  29.       else {
  30.         return TRUE;
  31.       }
  32.     }
  33.     return FALSE;
  34.   }
  35.   /**
  36.    * @see Cachebackend
  37.    */
  38.   public function IsNeedCreateNewCache($Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix)
  39.   {
  40.     clearstatcache();
  41.     if (file_exists($Key)) {
  42.       if ((time() - filemtime($Key) > $ExpiredPeriod - $SoonPeriod) && (
  43.           (!file_exists($Key . $SoonFuseKeyPostfix)))) {
  44.         return TRUE;
  45.       }
  46.       else {
  47.         return FALSE;
  48.       }
  49.     }
  50.     else {
  51.       return FALSE;
  52.     }
  53.   }
  54.   /**
  55.    * @see Cachebackend
  56.    */
  57.   public function PutCache($Key, $Data)
  58.   {
  59.     if($Key) {
  60.       @file_put_contents($Key, serialize($Data));
  61.     }
  62.     else {
  63.       throw new Exception('Cache key is empty');
  64.     }
  65.   }
  66.   /**
  67.    * @see Cachebackend
  68.    */
  69.   public function Rename($PreparedKey, $RenameKey)
  70.   {
  71.     return @rename($PreparedKey, $RenameKey);
  72.   }
  73. }
  74. ?>


Думаю теперь надо привести пример использования. Допустим, у вас есть объект $UserData, который возвращает одни данные c помощью метода GetData(), и функция GetUserText(), возвращающая другие данные. В обе функции передается параметр - идентификатор пользователя. Результат их работы складывается как строки и отдается на съедение браузеру. Мы закешируем эти данные на день и подготовим кеш объекта за час до конца дня, а кеш функции за два часа.


Listing №6 (PHP)
  1. <?php
  2. //Инициализируем пользователя
  3. $User = new User();
  4. //Инициализируем данные пользователя
  5. $UserData= new UserData($User->Id);
  6. //Настраиваем кеширование данных пользователя
  7. Cacher::SetCallback('GetData', array($User->Id), $UserData);
  8. //Получаем данные пользователя
  9. $Response = Cacher::GetData('userdata', md5($User->Id), 24 * 60 * 60, 60 * 60);
  10. //Настраиваем кеширование текста пользователя
  11. Cacher::SetCallback('GetUserText', array($User->Id));
  12. //Получаем текст пользователя
  13. $Response .= Cacher::GetData('usertext', md5($User->Id), 24 * 60 * 60, 120 * 60);
  14. //Отдаем результат
  15. echo $Response;
  16. ?>


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

 
Create comment
 
Formatting
Comment can not be edited. Please, use the button "Preview"
By
  (Enter prev char)
Comment
Categories
PHP
12
articles
Прочее
4
articles
Delphi
0
article
C/C++
0
article
C#
0
article
Java
0
article
Perl
0
article
Python
0
article
Enter
Cookie must be "ON"
Login
Password
 
Popular tags
PHP
9
articles
паттерн
5
articles
framework
5
articles
шаблон
5
articles
Template View
3
articles
Facade
3
articles
Service Stub
3
articles
Page Controller
2
articles
Singleton
2
articles
Gateway
2
articles
MySQL
2
articles
Registry
2
articles
Command
2
articles
Front Controller
2
articles
Action
2
articles
Abstract Factory
2
articles
типовое решение
2
articles
шаблоны проектирования
2
articles
Iterator
2
articles
Transaction Script
2
articles
Rambler's Top100 Правильный CSS!