Pull to refresh

Первичный кэш в Kohana 3 с использованием тегов

Reading time5 min
Views3.3K
Приведенный пример является результатом решения одной задачи, которая возникла при разработке системы управления сайтом на фреймворке Kohana 3.1, в которой предполагается одна учетная запись администратора и множество незарегистрированных читателей.

Требовалось надолго кэшировать результаты работы методов моделей, которые обращаются к базе данных. Фактически, требовалось создавать копии наборов данных из базы, чтобы снизить нагрузку на СУБД. Для немедленного обновления кэша при добавлении новых данных или обновления старых требовалась очистка кэша по тегам.

Учитывая все это, и в связи с ограничениями используемых хостингов требования были следующие:
  • Кэш должен храниться в файлах.
  • Кэш должен храниться долго, для увеличения скорости извлечения данных и снижения нагрузки на СУБД.
  • При обновлении данных администратором сайта, кэш, содержащий устаревшие данные должен очищаться, причем, очищаться должен не только кэш результатов функций, напрямую извлекающих эти данные из базы, но и тех, результаты которых связаны с этими данными (например, при удалении рубрики каталога должен очищаться кэш списков позиций рубрик). Для достижения этой цели должны поддерживаться теги.
  • Ради достижения цели можно в определенных рамках пожертвовать временем, уходящим на добавление новых материалов, и которое будет затрачено в том числе на очистку кэша, так они добавляются «своим человеком», а не сторонними пользователями.

Стандартный класс Cache_File не поддерживает теги, по этой причине потребовалось писать свой класс, ему было дано имя JetCache.

Класс спроектирован по шаблону «одиночка». Рассмотрим пример работы класса в модели для банка файлов. При инициализации модели создается экземпляр:

$this->cache = JetCache::instance();


Создание кэша данных рассмотрим на примере функции для извлечения списка файлов определенной рубрики (здесь из нее удалены некоторые аргументы и некоторый код для упрощения чтения):

    //Вернуть список файлов рубрики
    /**
     *
     * @param int $rubricId Id рубрики в БД
     * @return array
     */
    public function getFiles($rubricId) 
    {
        //!!! В случае, если удается получить информацию из кэша, 
        //вернуть эту информацию
        $key = 'filebank_get_files'.$rubricId;
        $arResult = $this->cache->get($key);
        if (is_array($arResult)) {
            return $arResult;
        }
        
        $arResult = array();
        $arParams = array();
        $arParams[':rubricId'] = $rubricId;

        $query = "
            SELECT 
                * 
            FROM 
                `filebank_files` 
            WHERE 
                `rubric_id`=:rubricId 
            ORDER BY 
                `time` DESC, 
                `name` ASC
        ";
        $arResult['files'] = DB::query(Database::SELECT, $query)
                ->parameters($arParams)
                ->execute()
                ->as_array();
        
        //!!! Внести в кэш результат работы
        $this->cache->set($key, $arResult, array('filebank_rubrics', 'filebank_files'));

        return $arResult;
    }


Таким образом, запись к кэше делается с ключом $key = 'filebank_get_files'.$rubricId и тегами «filebank_rubrics» и «filebank_files», то есть, очищать эту запись требуется при обновлении информации о рубриках и непосредственно файлах.

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

    protected $cacheRegExp = '/^filebank/';

    //Удалить рубрику
    public function delRubric($rubricId) 
    {
        $query = '
            SELECT 
                `name` 
            FROM 
                `filebank_files` 
            WHERE 
                `rubric_id`=:rubricId
        ';
        $arFiles = DB::query(Database::SELECT, $query)
                ->param(':rubricId', $rubricId)
                ->execute()
                ->as_array();

        $arFiles = Arr::path($arFiles, '*.name');
        $this->delFiles($arFiles);

        $query = '
            DELETE FROM 
                `filebank_rubrics` 
            WHERE 
                `rubric_id`=:rubricId
        ';
        DB::query(Database::DELETE, $query)
                ->param(':rubricId', $rubricId)
                ->execute();
        
        //!!! Очистка кэша по тегам
        $tags = array('filebank_rubrics', 'filebank_files');
        $this->cache->delete_by_tags($tags, $this->cacheRegExp);
    }

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

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

В случае больших проектов, где требуется высокая скорость очищения кэша или добавление информации пользователями, конечно, лучше использовать специальные решения. Например, драйверы «Memcached-tag» или «Xcache» модуля «Cache». Но для небольших сайтов, администрируемых одним человеком или небольшой группой людей, при использовании хостингов без предоставления специальных инструментов для кэширования, это решение подходит хорошо.

Файлы, в которых хранится кэш, содержатся в одной директории и имеют следующую структуру:
Время, после которого запись считается устаревшей (unix timestamp)\n
Список тегов через запятую\n
Сериализованные данные\n


Наконец, приведу полный код класса:
<?php defined('SYSPATH') or die('No direct access allowed.');

class JetCache
{
    protected static $instance = NULL;
    protected static $config;
    protected static $cache_dir;
    protected static $cache_time;
    
    
    public static function instance()
    {
        if (is_null(self::$instance)) {
            self::$instance = new self();
        }
        
        return self::$instance;
    }
    
    protected function __construct()
    {
        self::$config = Kohana::config('jethelix')->default;
        self::$cache_dir = self::$config['jet_cache_dir'];
        
        if (!is_dir(self::$cache_dir)) 
        {
            $oldUmask = umask(0000);
            
            if (!mkdir(self::$cache_dir, 0777, TRUE)) {
                $message = 'Неверная директория для модуля JetCache';
                throw new Exception($message);
            }
            
            umask($oldUmask);
        }
        
        self::$cache_time = self::$config['jet_cache'];
    }
    
    protected function __clone() {
    }
    
    public function set($id, $data, array $tags=array(), $lifetime=NULL)
    {
        if (!$lifetime) {
            $lifetime = self::$cache_time;
        }
        
        $filename = self::$cache_dir . '/' . $id . '.txt';
        
        $expires = time() + (int)$lifetime;
        $tagString = implode(',', $tags);
        $serData = serialize($data);
        
        $content = $expires . "\n" . $tagString . "\n" . $serData;
        
        try {
            file_put_contents($filename, $content);
        }
        catch (Exception $e) {
            return FALSE;
        }
        
        return TRUE;
    }
    
    public function get($id)
    {
        $filename = self::$cache_dir . '/' . $id . '.txt';
        
        if (!is_file($filename)) {
            return NULL;
        }
        
        try {
            $content = file_get_contents($filename);
        }
        catch (Exception $e) {
            return NULL;
        }

        $arContent = explode("\n", $content);
        unset ($content);
        
        try {
            if ($arContent[0] < time()) {
                return NULL;
            }
            
            $data = unserialize($arContent[2]);
            return $data;
        }
        catch (Exception $e) {
            return NULL;
        }
    }
    
    public function delete($id)
    {
        $filename = self::$cache_dir . '/' . $id . '.txt';
        
        try {
            unlink($filename);
        }
        catch (Exception $e) {
            return FALSE;
        }
        
        return TRUE;
    }
    
    public function garbage_collect()
    {
        $dir = opendir(self::$cache_dir);
        while ($file = readdir($dir)) 
        {
            $fullName = self::$cache_dir . '/'. $file;
            if (!is_file($fullName)) {
                continue;
            }
            
            try {
                $this->_deleteIfExpires($fullName);
            }
            catch (Exception $e) {
                return FALSE;
            }
        }
        
        return TRUE;
    }
    
    protected function _deleteIfExpires($filename)
    {
        $fhandle = fopen($filename, 'r');
        $expires = (int)fgets($fhandle);
        fclose($fhandle);
        
        if ($expires < time()) {
            unlink($filename);
        }
    }
    
    public function delete_by_tags(array $tags, $filenameRegExp=NULL)
    {
        $this->garbage_collect();
        
        try {
            $arFiles = $this->_getTaggedFiles($tags, $filenameRegExp);
            $this->_deleteFiles($arFiles);
        }
        catch (Exception $e) {
            return FALSE;
        }
        
        return TRUE;
    }
    
    protected function _getTaggedFiles(array $needTags, $filenameRegExp)
    {
        $taggedFiles = array();

        $dir = opendir(self::$cache_dir);
        while ($file = readdir($dir)) 
        {
            $fullName = self::$cache_dir . '/' . $file;
            if (!is_file($fullName)) {
                continue;
            }

            if ($filenameRegExp && !preg_match($filenameRegExp, $file)) {
                continue;
            }
            
            $hasTags = $this->_getTagsFromFile($fullName);            
            $isValid = $this->_tagsValidate($needTags, $hasTags);
            if ($isValid) {
                $taggedFiles[] = $fullName;
            }
        }
        
        return $taggedFiles;
    }
    
    protected function _getTagsFromFile($filename) 
    {
        $fhandler = fopen($filename, 'r');
        fgets($fhandler);
        $tagString = fgets($fhandler);
        fclose($fhandler);
        
        $tagString = trim($tagString);
        $arTags = explode(',', $tagString);
        
        return $arTags;
    }

    protected function _tagsValidate(array $needTags, array $hasTags)
    {
        foreach ($needTags as $tag) {
            if (in_array($tag, $hasTags)) {
                return TRUE;
            }
        }
        
        return FALSE;
    }    
    
    protected function _deleteFiles(array $files)
    {
        foreach ($files as $filename) {
            unlink($filename);
        }
    }
}
Tags:
Hubs:
+12
Comments31

Articles