Интересный подход для кэширования моделей

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

    Реализация идеи на Zend Framework:
    1. Все модели наследуются от нашего класса My_Db_Table_Abstract:
    class My_Model_ModuleSettings extends My_Db_Table_Abstract

    2. Который в свою очередь наследуется от Zend_Db_Table_Abstract:
    class My_Db_Table_Abstract extends Zend_Db_Table_Abstract

    3. В базовом для всех моделей классе My_Db_Table_Abstract описываем магический метод __call():
    public function __call($name, $arguments)
    {
      /** If call cached method */
      if (preg_match('/^cached_(.+)$/', $name, $methodName)&&method_exists($this,$methodName[1])) {
        /** Get cache instance */
        $cache = My_Cache::getInstance();
        /** Get arguments hash */      
        $argHash = md5(print_r($arguments, true));      
        /** Get model class name */
        $className = get_class($this);
        /** If method result don't cached */
        if (!$result = $cache->load('model_'.$className.'_'.$methodName[1].'_'.$argHash)) {
          $result = call_user_method_array($methodName[1], $this, $arguments);
          $cache->save($result,
                       'model_'.$className.'_'.$methodName[1].'_'.$argHash,
                       array('model',$className,$methodName[1]));
        }
        return $result;
      } else {
        /** Generate exception */
        throw new Exception('Call to undefined method '.$name);
      }
    }


    Теперь у нас появилась возможность использовать методы моделей двумя способами:
    1. Не кэшируя, просто обратившись к методу:
    $result = $this->_life->getAll('Now!!');

    2. Кэшируя, дописав к имени метода префикс «cached_»:
    $result = $this->_life->cached_getAll('Now!!');


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

    Некоторые нюансы:
    1. Для того чтобы кэш результата выполнения метода был разный при отличающихся параметрах метода я сделал хеширование параметров:
    $argHash = md5(print_r($arguments));
    Это, пожалуй, самый неоднозначный момент, т.к. я не могу точно сказать, как это может повлиять на производительность (при тестировании увеличение нагрузки замечено не было). При этом можно использовать разные функции хеширования(md5(),sha1()..) и разные способы приведения массива переменных к строковому типу (print_r(), var_dump(), implode()).

    2. Имя кэшируемого файла имеет вид model_ИмяКласса_ИмяМетода_ХэшПараметров, исключая возможность совпадения.

    3. Кэш помечается тегами 'model', 'className' и 'methodName', которые позволяют легко манипулировать очисткой. Вот для примера очистка кэша всех методов модели My_Model_ModuleSettings:
    $cache->clean(
      Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG,
      array('My_Model_ModuleSettings')
    );


    Очень хотелось бы услышать ваши комментарии… Какие вы видите недостатки этого способа? Имеет ли он право на жизнь?
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 55

      +3
      А зачем работать с кешэм явно?
        +8
        Т.е. зачем это разделение на обычный и cached_ методы?
          +2
          Есть же методы, которые не нуждаются в кэшировании (например какой-нибудь метод генерации псевдослучайной величины на основании поступивших данных). Наверняка можно придумать метод, который в одном месте вызова желательно кэшировать, а в другом нет. При этом по имени в контроллере сразу видно какие методы кэшируются, а какие нет. Да и в префиксе суть метода. Благодаря ему срабатывает метод __call.

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

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

          Я не пытаюсь развязать холивар. Наоборот хочу узнать — какие вы видите недостатки способа.
            +1
            кто определяет, должен ли метод кешироваться или нет?
            почему бизнес логика, а не модель, которая, по идее, обладает большей информацией о том, что и как лучше кешировать?
        +10
        Зачем использовать preg_match() для поиска префикса 'cached_', если достаточно использовать substr()?
          0
          я так понял что preg_match() в данном случае нужен не столько для поиска, сколько для выкусывания того, что идет после 'cached_'
            –1
            $parts = explode('_', $name);
            if ('cached'==$parts[0] && method_exists($this,$parts[1]) ) {
            

            ?
              +5
              Ну или if (substr($name, 0, 7) == 'cached_') { $method = substr($name, 7);… }
                +2
                2 ворнинга при обращении к несущетвующим индексам массива, если имя не содержит '_'.
                  +1
                  $parts = explode('_', $name);
                  if (sizeof($parts)==2 && 'cached'==$parts[0] && method_exists($this,$parts[1]) ) {
                  

                  )
                    +2
                    Может быть поэтому используют preg_match, т.к. он лежит на поверхности.
                      0
                      вы ведь прекрасно знаете зачем придумали регулярные выражения…
                      чтобы описывать легко СЛОЖНЫЕ шаблоны.
                      проверить что строка начинается со слова «cached_» — это не сложный шаблон, и логично использовать обычные строковые функции.

                      быть логичным или нет — каждый выбирает сам)
                      +2
                      $method = 'cached_method';
                      if (strtok($method, '_') == 'cached') echo 'а метод-то: '.strtok('_');
                –10
                Ну во первых читал на одном из форумов про медлительность substr. Парень утверждает, что ускорил работу шаблонизатора Смарти в 5 раз заменив где только можно substr на preg_match.
                Ну и во вторых это просто эстетичнее выглядит на мой взгляд. Люблю регулярные выражения.
                  +2
                  А Вы сами пробовали тестировать? Не знаю, как там в шаблонизаторах, а у меня на скорую руку substr() примерно в полтора раза быстрее отрабатывает, чем preg_match().
                  А насчет эстетичности и любви к регуляркам — тут Вы противоречите стремлению к оптимизации. Да и несерьезно это, чистые эмоции ;) Про substr() забывать не стоит.
                    0
                    Это утверждение противоречит всем тестам сравнения производительности substr и preg_match при равных условиях. Советую вам усомниться в результатах опубликованных на форуме и провести свои.
                      +1
                      Да, я уже понял. Просто при написании кода не задумался даже… Прочитал, в памяти отложилось и как аксиома…
                      +3
                      гениально =) причину тормозов смарти надо всегда искать в нативных php`шных функциях для работы со строками =)
                    +1
                    а кеш получается вечный? Где задается время жизни кеша?
                    Ведь для разных моделей нужно будет разное время обновления информации
                      +1
                      При первой инициализации $cache создаётся объект Zend_Cache в котором прописывается время жизни кэша. Время жизни в данном примере не вечное, но для всех моделей одинаковое. Спасибо за комментарий — наду будет доработать, но вообще нужные кэш-файлы чистятся при определённых событиях и необходимости автоматической очистки кэша у меня пока не возникало.
                      +2
                      Идея конечно хорошая, сам к этому пришел. Но реализация…

                      Зачем же существует ActiveRecord?! И словосочетание ObservedCache вам не о чем не говорит?
                        +1
                        Что-то гугл тоже не знает про «словосочетание ObservedCache». Может конечно не так искал.

                        Расскажите?
                          0
                          Создаем класс, который будет следить за моделями.
                            +1
                            Само отправило… Странно… Так вот пример.
                            class ListSweeper < ActionController::Caching::Sweeper
                            observe List, Item

                            def after_save(record)
                            list = record.is_a?(List) ? record : record.list
                            expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id)
                            expire_action(:controller => "lists", :action => "all")
                            list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
                            end
                            end

                            По строчкам
                            observe List, Item — модели, за которыми он следит
                            after_save(record) — функция, выполняется если хотя бы одна модел создалась или обновилась
                            list = record.is_a?(List)? record: record.list — обновилась одна запись или список
                            expire_page — очистка страничного кеша
                            expire_action — очистка активного кеша
                            list.shares.each { |share| expire_page(:controller => «lists», :action => «show», :id => share.url_key) } — очистка группы страниц
                        0
                        1) The call_user_method_array() function is deprecated as of PHP 4.1.0.
                        2) $argHash = md5(print_r('',$arguments));
                        может $argHash = md5(print_r($arguments,true));?
                          0
                          Ой, да… Извиняюсь. Первоначально у меня вместо print_r(), было implode()… Название функции исправил, а параметры видимо забыл изменить.
                            0
                            лучше таки serialize, а не print_r, учитывая, что:
                            This function<print_r> uses internal output buffering with this parameter so it can not be used inside an ob_start() callback function.
                          +2
                          По мне так, довольно порочная практика. Потому как мы при работе с моделью должны держать в голове специфику ее работы: как она хранит данные, что случается за интерфейсом при вызове метода. Т.е. согласитесть, $model->cached_insert($row) будет работать уже совсем не так, как хотелось бы. Плюс кастомные методы модели, например, $employees->payTo($id)… А что будет если мы вызовем cached_payTo($id)? Т.е. логика работы с данными выносится за рамки класса. Мне кажется, что модель сама должна определять, что можно кэшировать, а что нет. Хотя для мелких проектов с парой-тройкой контроллеров может быть так и удобнее.
                            0
                            Хмм… Наверное соглашусь с Вами, что для крупных проектов данный подход не очень удобен. Хотя мне кажется программист в здравом уме не будет кэшировать метод с именем insert()… Да и любой другой, если не знает что за ним лежит. Получается есть свобода, а как ей пользоваться это уже от программиста зависит.
                              +1
                              Так вот, порочная практика — это полагаться на здравый ум программиста… Как показывает опыт — это последнее на что можно полагаться, особенно если работаешь не один :))

                              Модель-обсервер не будет работать (кто-то хочет оповещать других о получении данных), или модель которая работает с каким-нибудь сингелтоном (тот же Zend_Registry)… Не знаю даже, что еще можно притянуть. :))) Вообщем, при росте проекта можно получить небольшой гемор (а куда без него), а так вполне нормальное решение.
                                –3
                                С вероятностью в 99%, если интерфейс позволяет, программисту это обязательно когда нибудь понадобиться. Так, что делайте интерфейс проще и безопасней. Хотя не вижу ничего плохого в кешировании метода insert, который возвращает последний добавленный id — очень удобно кстати.
                                  0
                                  Вы меня заинтересовали.
                                  Объясните, пожалуйста, простым смертным что «неплохого» в кешировании метода insert?
                                    0
                                    Кешировать insert не лучшая идея. Думаю не стоит развивать эту тему дальше.
                              0
                              Подсказок на cached_* методы вы, скорее всего, лишитесь (если ваша IDE не поддерживает @method или если программисты не будут очень дисциплинированы).

                              Лично я предпочитаю явно объявлять все методы. Мой вариант выглядел бы примерно так:
                              function getXyz() {}
                              function getXyzCache() {
                                  if (!$result = $cache->load('можем по-человечески определить ключ')) {
                                    $result = $ths->getXyz();
                                    $cache->save($result, 
                                                 'key'
                                    );
                                  }
                              }<pre>
                              
                              Бонус - со значением, которое кладется в или вынимается из кэша - можно провести еще какие-то манипуляции, если это нужно.
                              А условие if (!$result = $cache->load()) - можно при желании вынести в дополнительную прослойку:
                                0
                                черт, отправилось раньше…
                                прослойка, примерно:
                                function cache_get_and_store($key, $callback){
                                  if (!$result = $cache->load($key)) {
                                    // достаем из базы или откуда там надо, ну и сохраняем, знамо дело.
                                  }
                                }

                                Можно и ОО-решение, само собой.
                                  0
                                  Для сеттеров и геттеров в PHP и были добавлены magic methods.
                                  Конечно IDE не подскажет о их существовании, но взамен вы получаете чистый интерфейс без нескольких десятков set, get методов.
                                    +1
                                    По поводу phpDoc'ов соглашусь, но неудобств не много.
                                    1. Пишете $this->life->
                                    2. Вылетает подсказка с доступными методами и их описанием
                                    3. Выбираем нужный и дописываем префикс

                                    Зато в минусы я отношу дублирование кода для каждого метода и ручное определение ключей(которые Вы отнесли к плюсам). Зная по какому алгоритму даются ключи, можно легко манипулировать очисткой, а в вашем случае каждый конкретный метод может иметь свой алгоритм.
                                      0
                                      Что ж, во многом это дело вкуса. Я предпочитаю явный интерфейс, вы — магические штучки ;) So be it.

                                      Насчет производительности: в нашем проекте сейчас используется подход, очень похожий на ваш (правда, я надеюсь его изменить), так вот, сильных тормозов не наблюдаем, несмотря даже на использование serialize.
                                    0
                                    Также вместо print_r можно использовать serialize для приведения массива к строке
                                      0
                                      Будет работать намного медленнее.
                                        0
                                        Неправда:

                                        Вот тест на 10000 проходах для массива из 1000 строк+1000 чисел, случайно перемешанных.

                                        array(3) {
                                        [«serialize»]=>float(14.9195878506)
                                        [«json»]=> float(15.6144771576)
                                        [«print_r»]=> float(28.0943210125)
                                        }

                                        gist.github.com/743578
                                      0
                                      я думаю что стоит сделать флаг для кэширования в бутстрапе можно его в Zend_Registry записать ещё.
                                      И включать и выключать кэширование по этому флагу и оставить метод fetchAll для всего

                                      Кстати не замечали ли вы что при одном человеке на сайте кэширование работает медленне чем напрямую из базы данных? я думаю что это связанно с сериализацией/ десериализацей объектов которые хранятся в кэше.

                                      Можно составить графики скорости выполнения sql запросов в зависимости от кол — ва пользователей. И в том месте где уже эффективнее использовать кэширование устанавливать флаг в true.
                                        0
                                        почему кэш ($argHash) зависит от имени модели и метода, а не от внутреннего состояния?

                                        т.е. для двух новостей всегда будет возвращаться один и тот же результат?

                                        $cache->load('model_'.$className.'_'.$methodName[1].'_'.$argHash)

                                        в этой строке совсем никак не учтен PK сущности.
                                          0
                                          а, пардон, вы же кешируете массив сущностей, дописывая прокладку к тейбл адаптеру, сори…
                                          0
                                          preg_match('/^cached_(.+)$/',....)
                                          следовало бы заменить бы на:
                                          strpos(...)

                                          ps: ровно то же самое реализовывал ~ 1.5 года назад, только вместо print_r($args,1) применял serialize(), а дата создания кеша сохранялась в дату модификации файла.
                                            0
                                            и еще я немного не понял, вы заведомо собираетесь по всей системе писать либо вызовы с кешируемыми методами либо нет. То есть возможность отключить кеширование одной настройкой не выйдет, либо постоянно будут костыли вида
                                            if($cacheEnabled){
                                            $result = $this->_life->getAll('Now!!');
                                            }else{
                                            $result = $this->_life->cached_getAll('Now!!');
                                            }

                                            нужно додумать что-то универсальное, зависящее от конфига, с возможностью задания времени актуальности кеша (не знаю есть ли в Zend такая возможность)
                                            0
                                            Муть, кешированием данных должна управлять каждая модель сама, т к она лучше знает что это за данные, как часто они обновляются и т.д. И просто класть в кеш на определенное время данные — неэффективно, ведь они вполне могут уже обновиться в БД. Про возможность кеширования стоило думать до разработки проекта, а не после, теперь вы вынуждены будете пристраивать уродливые неэффективные костыли.
                                            // И кстати отказ от Zend Framework думаю тоже мог бы ускорить работу :)
                                              0
                                              ускорить работу кода или программистов?
                                                0
                                                А это уже другой вопрос :)
                                              +1
                                              Является ли метод потенциально кэшируемым должен определять не программер, класс использующий, а сам метод.

                                              Т.е., как указывалось выше, надо в самом начале детерминированного метода (без side effect'ов и, не зависящий от окружения) поставить проверку на наличие результата в кэше и возврат его в случае успеха.

                                              Чтобы не заниматься копипэйстом эту функциональность можно выделить в одно место. И это, кстати, скорее будет метод My_Cache, принимающий на вход callback (хотя его и из стека можно вытащить) и arguments. Кстати, хорошая идея для доп. функциональности Zend_Cache :)

                                              PS Но что меня все-таки смущает, это уникальность хэша.
                                                +1
                                                Чем этот вариант лучше использования Zend_Cache_Frontend_Class?
                                                Идея хорошая, но, по-моему, автор придумал велосипед.
                                                Причем разработчики Zend-а придумали его лучше. Выше уже кто-то писал: логика кеширования должна быть инкапсулированна внутри класса.
                                                  +1
                                                  Zend_Cache_Frontend_Function и Zend_Cache_Frontend_Class не подошли? Почему?
                                                    +1
                                                    потому что людям, что б их использовать, нужно еще документацию читать.
                                                    0
                                                    Я бы посоветовал вам прогнать профайлером свой код и определить узкие места, которые необходимо закэшировать или просто определить их методом здравого смысла. На мой взгяд не нужно кэшировать все методы модели. Непосредственно в методах модели, в табличном гейтвее как у вас или вообще на уровне гейтвея в домен, прописать логику кэширования. Для удобвства можно вынести настройки в конфигурационный файл, (лучше чем в бутстрапер).

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

                                                      <?php
                                                      class App_Cache
                                                      {
                                                          public static $frontendName = 'Class';
                                                          public static $backendName  = 'File';
                                                              
                                                          public static $frontendOptions;
                                                          public static $backendOptions = array('cache_dir' => CACHE_DIR);
                                                          
                                                          static public function get($class)
                                                          {
                                                              $instance = new $class;
                                                              self::$frontendOptions = array(
                                                                  'cached_entity' => $instance
                                                              );
                                                              return Zend_Cache::factory(self::$frontendName,
                                                                                         self::$backendName,
                                                                                         self::$frontendOptions,
                                                                                         self::$backendOptions); 
                                                          }
                                                      }
                                                      


                                                      В контроллере это используется так:

                                                      $modelPages = App_Cache::get('Model_Page');
                                                      $list = $modelPages->getList();
                                                      

                                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                      Самое читаемое