Zend Framework

индекс
75,79

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

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

Реализация идеи на 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')
);


Очень хотелось бы услышать ваши комментарии… Какие вы видите недостатки этого способа? Имеет ли он право на жизнь?
+11
19 октября 2009, 23:49
27

комментарии (55)

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

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

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

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

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

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

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

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

Расскажите?
0
le0pard #
Создаем класс, который будет следить за моделями.
+1
le0pard #
Само отправило… Странно… Так вот пример.
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
chEbba #
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
TuiKiken #
Ой, да… Извиняюсь. Первоначально у меня вместо print_r(), было implode()… Название функции исправил, а параметры видимо забыл изменить.
0
TheShock #
лучше таки 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
jamayka #
По мне так, довольно порочная практика. Потому как мы при работе с моделью должны держать в голове специфику ее работы: как она хранит данные, что случается за интерфейсом при вызове метода. Т.е. согласитесть, $model->cached_insert($row) будет работать уже совсем не так, как хотелось бы. Плюс кастомные методы модели, например, $employees->payTo($id)… А что будет если мы вызовем cached_payTo($id)? Т.е. логика работы с данными выносится за рамки класса. Мне кажется, что модель сама должна определять, что можно кэшировать, а что нет. Хотя для мелких проектов с парой-тройкой контроллеров может быть так и удобнее.
0
TuiKiken #
Хмм… Наверное соглашусь с Вами, что для крупных проектов данный подход не очень удобен. Хотя мне кажется программист в здравом уме не будет кэшировать метод с именем insert()… Да и любой другой, если не знает что за ним лежит. Получается есть свобода, а как ей пользоваться это уже от программиста зависит.
+1
jamayka #
Так вот, порочная практика — это полагаться на здравый ум программиста… Как показывает опыт — это последнее на что можно полагаться, особенно если работаешь не один :))

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

Лично я предпочитаю явно объявлять все методы. Мой вариант выглядел бы примерно так:
function getXyz() {}
function getXyzCache() {
    if (!$result = $cache->load('можем по-человечески определить ключ')) {
      $result = $ths->getXyz();
      $cache->save($result, 
                   'key'
      );
    }
}<pre>

Бонус - со значением, которое кладется в или вынимается из кэша - можно провести еще какие-то манипуляции, если это нужно.
А условие if (!$result = $cache->load()) - можно при желании вынести в дополнительную прослойку:
0
crocodile2u #
черт, отправилось раньше…
прослойка, примерно:
function cache_get_and_store($key, $callback){
  if (!$result = $cache->load($key)) {
    // достаем из базы или откуда там надо, ну и сохраняем, знамо дело.
  }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<?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();

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