cache dependecies - покритикуйте

значит столкнуля с таким случаем

ммм

для начала расскажу как это все делалось и делаеться




protected function afterSave(){

  Yii::app()->cache->delete('__KEY__');

}


public static function getAllRegions(){

  $cache = Yii::app()->cache;

  $regions = $cache->get('__KEY__');


  if(!$regions){

     $regions = Region::model()->findAll();

     if($cache && $regions)

         $cache->set(..., $regions);

  }


  return $regions;


}




  • достаточно легко игвалидировать кеш если что то изменилось
  • в кеш суеться массив объектов (много памяти)

  • плохо работает с зависмостями например нельзя получить из кеша $region->cities

нужно будет вызывать City::getAllCities($regionId) что в свою очеред снова будет кешировать объекты

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

и придумал другой способ

создаем таблицу (можно в памяти) а можно и просто ассоциативный массив прям сразу в кеш всунуть

дальше буду использовать слово таблица

таблица представляет собой key-value хранилище, где value - по сути обычный счетчик

расширяем класс CActiveRecord

добавляем метод, скажем, invalidateCache($keys)

инвалидация ключа происходит путем простого увеличения счетчика.

вызываеться после удаления, обновления или еще гдето…

добавляем метод getKeyDependency($key) который по ключу строит нашу зависимость

далее гдето в коде мы уже используем не Region::getAllRegion()

а


$regions = Region::model()->cache(0, $this->getKeyDependency('some key'))->findAll();

или же


$cities = $region->cache(0, $this->getKeyDependency($region->id)))->cities;

где последнее cities связь HAS_MANY описанная в Region::relations

инвалидировать же города в регионе мы сможем, скажем так


class City extends OurNewClass{


  protected function afterSave(){

    $this->invalidateCache(array($this->regionId));

  }

}

  • храним не массив моделей, а поменьше (все равно объект но памяти жрет меньше)

  • сама таблица с ключами будет оооочень небольшой обрабатываться будет быстро

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

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

так что явных минусов не вижу.

Предвижу ваши вопросы по поводу как формировать ключи в таблице, ну например так




function invalidateCache($keys){

  foreach($keys as $key){

    $key = md5($this->tableName() . $key);

    //....

  }

}



  • вообщем как вам идея?

  • есть ли какието предложения по поводу как ее улучшить?

  • или может вообще ее выбросить?

  • формирование ключей что я показал конечно не ахти - будут проблемы при таком подходе!

    но может есть идеи как и это поправить?

единственное что не могу точно сказать, это как можно будет реализовать

кеширование при запросе, к примеру, такого вида




 $regions = Region::model()->with('cities')->findAll();



и как оно будер работать для разных значений together в описании связи

или в таком случаее имеет смысл использовать


 $regions = Region::model()->cache(...)->with('cities')->findAll();

короче вот на скорую руку




class ActiveRecord extends CActiveRecord

{

    public function cache($duration, $dependency = null, $queryCount = 1)

    {

        if(is_string($dependency))

        {

            $dependency = new CounterCacheDependency($dependency);

        }

        return parent::cache($duration, $dependency, $queryCount);

    }

}






class CD

{

    private static $_cache;

    

    public static function invalidate($aKeys)

    {

        if(is_string($aKeys))

            $aKeys = array($aKeys);

        

        self::getCache();

        

        foreach($aKeys as $key)

        {

            $counter = self::$_cache->get(CounterCacheDependency::HASH_KEY_PREFIX.$key);

            if($counter === false)

            {

                if(function_exists('mt_rand'))

                    $counter = mt_rand(0, 2147483647);

                else

                    $counter = rand(0, 2147483647);

            }

            else

            {

                $counter++;

            }

            self::$_cache->set(CounterCacheDependency::HASH_KEY_PREFIX.$key, 0, 0);

        }

    }

    

    public static function get($aKey)

    {

        return new CounterCacheDependency($aKey);

    }


    private static function getCache()

    {

        if(empty(self::$_cache))

        {

            self::$_cache = Yii::app()->cache;

            if(empty(self::$_cache))

            {

                throw new CException(__CLASS__.': cache is not valid');

            }

        }

        return self::$_cache;

    }

}






class CounterCacheDependency extends CCacheDependency

{

    const HASH_KEY_PREFIX = 'CACHE_HASH_KEY';

    

    private static $_cache;

    

    private $_counter;

    

    public function __construct($aKey)

    {

        self::getCache();


        $internalKey = self::HASH_KEY_PREFIX.$aKey;

        

        $this->_counter = self::$_cache->get($internalKey);

        if($this->_counter === false)

        {

            if(function_exists('mt_rand'))

                $this->_counter = mt_rand(0, 2147483647);

            else

                $this->_counter = rand(0, 2147483647);

            

            self::$_cache->set($internalKey, $this->_counter, 0);

        }

    }


    protected function generateDependentData()

    {

        return $this->_counter;

    }

    

    private static function getCache()

    {

        if(empty(self::$_cache))

        {

            self::$_cache = Yii::app()->cache;

            if(empty(self::$_cache))

            {

                throw new CException(__CLASS__.': cache is not valid');

            }

        }

        return self::$_cache;

    }

    

}



как это выглядит в коде




    protected function afterSave()

    {

        CD::invalidate(array(

            'region.country',

            'region.country'.$this->countryId

        ));

    }


....

    $countries = GeoCountry::model()->cache(0,'all.country')->findAll();

....

    $operators = Operator::model()->cache(0, 'operator'.$aCountry->id)->findAllByAttributes(array('countryId'=>$aCountry->id));




теперь вот думаю может invalidate перенести в ActiveRecord…

все равно не нравиться

самы большой плюс первого метода, от корого не хочеться отказываться, это то что ключ кеша не выходил за рамки класса/объекта в котором он использовался

сейчас же приходиться смотреть - лазить в клас в котором кеш инвалидируеться и смотреть, а правильноый ли я ключ использовал для получения данных

короче придеться как то либо __get переопределить, либо __call

либо вообще написать getCachedRelation($relation)

тут ведь как

если задать к примеру


$models = Region::model()->with('country')->cache(...)->findAll();

то есть, если with перед вызовом cache, еще есть возможность определить зависимость (ключ для кеша)

если будет стоять после cache() уже не определим

точно так же касательно scopes (хотя наверно поддержку скоупов я делать не буду)

вообщем походу cache() (еще раз оговорюсь - именно для моего случая) должен вызываться сразу перед методами find*

если же вызовы идут типа


$country->cache()->regions;

то уже нет простого способа определить ключи для кеша

остаеться только както… скажем использовать вот так


$regions = $country->regions_cached;

Уж извините, всё не осилил, но возникли следующие мысли:

  1. Кэширование внутри модели меня очень сильно смущает с точки зрения логики. Я думаю, что модель не должна решать, что и как ей кэшировать. Сегодня нужно кэшировать так, завтра иначе, следует ли из-за этого каждый раз лезть в модель?

  2. Я люблю, когда всё выглядит просто. Тут мне всё кажется каким-то шаманским :) Нужно ли кэшировать результаты самих запросов? Может быть будет достаточно закэшировать, например, виджет, который выводит результат? В общем, KISS.

  3. Возникают сомнения по поводу кэшировния связанных данных. Получается, если связанные данные обновились, а "основной" объект - нет, то кэш будет считаться актуальным?

хоть кто то ответил ;)

  1. очень часто специфика предполагет получать все данные. Тоесть не все, а много за раз.

ну вот к примеру во вьюшке нужно вывести select для выбора страны

я могу сделать просто


Country::getAll()

а в getAll тусануть код работы и с кешем и с БД

а могу тоже самое перекинуть в представление, но как то рагульно оно будет выглядеть, согласны?

единственное что приходит на ум это перекинуть это все в контроллер

  1. поцелуй конечно хорошо, я тоже иду от этого. может только взгляды разные. Да и код еще не допилен до нужной кондиции.

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

  1. да будет считаться валидным

ну вот к примеру

есть страна есть регионы

добавили новый регион

соответсвенно нужно инвалидировать кеш для запроса с выборкой по какойто конкретной стране

или если ошиблись в названии страны, исправили, выборака регионов по идентификатору страны все равно осталась валидной

удалили страну - нивалидировали кеши по стране для городов и регионов

  1. согласно KISS и логичности

куда еще можно засунуть статический метод getAll() как не в саму модель

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

б. более того модель может обновиться из разных мест опять же логичнее инвалидировать кеш в одном месте чем незабыть сделать тоже самое (повторить код без ошибок) во многих.

я вот имеено из этого подхода

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

да конечно есть места которые вообще не так будут кешироваться и работать и пр.

ну а на счет шаманства - да весь йии одно сплошное шаманство, потому и пользуеться популярностью :D

ха оказываеться все придумано до нас…как я мог забыть эту презентацию с highload.ru

если так разобратсья я сейчас пытаюсь сделать тегирование кешей но для Yii

вот вообщемто то статья http://www.opennet.ru/base/dev/memcached_tips.txt.html (скорее всего была написана после презентации)

ну и вот здесь можно почитать http://dklab.ru/chicken/nablas/47.html

вообщем ушел копать глубже

главное, когда реализуем - не забыть отписаться ;)

Конечно, кэшировать результат запроса хорошо, но, если я правильно понимаю, преобразование к объектам будет происходить каждый раз (тут уже могут возникнуть проблемы с памятью, если объектов очень много). Гораздо “красивее” выглядит вариант, когда закэширован сам HTML список, а не какие-то там данные, из которых еще его нужно будет сформировать. $this->beginCache() в представлении, согласен, смотрится плохо, и там могут быть вопросы с инвалидацией этого фрагмента, но зато тут всё предельно просто :) К слову, вопросами кэширования я серьезно не занимался, так что мои рассуждения основаны лишь на моём небогатом в этом вопросе опыте.

да я тоже както раньше не очень этим занимался

а теперь все из расчета на высокую нагрузку нужно делать

конечно представления или виджеты будут кешироваться

но хочеться что то универсальное для инвалидации группы кешей сделать

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

вернувшись к регионам (ну это как пример - не цепляйтесь сильно к слову)

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

например при добавлении нового региона в страну

  • инвалидировать кеш для внутреннего алгоритма

  • инвалидировать кеш для формы регистрации и формы обновления профиля

при изменении названия региона

  • к первым двум еще добавить инвалидацию кешей профилей пользователей относящихся к данному региону

(пример конечно так себе, но думаю идея понятна)

тегирование кешей - самое оно

инвалидация кешей проходит в модели - бо она знает когда она обновляеться

(для DAO конечно нудно будет вызывать инвалидацию явно, а не по событию)

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

у нас часто (по наблюдениям) тегом кеша может выступать "имя модели"[."имя связи"[."какойто первичный ключ"]]

а за количеством объектов будем следить - никуда е денемся

че то меня застопорило на словах HTML и widget

согласен, впринципе иногда более правльно кеширвоать не сырые данные запроса, а уже результат какихто преобразований над ними. Уже местами так и сделано.

По поводу тегирования кешей.

У нас почти все кеши имеют "вечное время жизни", и инвалидируються вручную.

Cache dependencies которые есть в Yii, ди а те которые я успел выше написать - это не совсем то что нужно.

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

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

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

Идея тегов мне тоже очень понравилась.

А вообще используя ZF мне очень нравилось использовать кеширование функции.

Достаточно просто было это дело автоматизировать.

А смысл в том что если в функцию приходят эквивалентные параметры это значит что она может вернуть закешированный результат для этих параметров.

Если честно я хотел нечто похожее в Yii.

типичный пример в Yii выглядел бы так:


class MyModel extends UActiveRecord{

...

public static funcion getListCachingFuncions(){

return array(

'func1','func2','getOnlineUsers'

);

}


public static function getOnlineUsers($param1,$param2,$param3){

//do something

}

...

наконецто дошли руки до этого тикета

с уже нормальной и протестированной реализацией

не совсем мне решение нравитсья - бо сильно много в ядре всего защищено и приватизировано )) поєтому существуют лишнее ввызовы методов.

но вообщем то теперь можем работать с тегированными кешами

код совсем не сложный - думаю что пояснений с тем как работать не нужно

но если вдруг… то конечно… )))

[spoiler]

TaggedMemCache




<?php


namespace application\components\cache;


/**

 *

 * @author alex

 * @date 19.07.12

 * @time 12:59

 * @file TaggedMemCacheache.php

 */

class TaggedMemCache extends \CMemCache

{

    protected $_invalidCache;


    /**

     * @see \CCache::get()

     *

     * @param string $id

     *

     * @return bool|mixed|string

     */

    public function get($id)

    {

        $value = $this->getValue($this->generateUniqueKey($id));

        if($value===false || $this->serializer===false)

            return $value;

        if($this->serializer===null)

            $value=unserialize($value);

        else

            $value=call_user_func($this->serializer[1], $value);

        if(is_array($value) && (!$value[1] instanceof \ICacheDependency || !$value[1]->getHasChanged()))

        {

            \Yii::trace('Serving "'.$id.'" from cache','system.caching.'.get_class($this));

            $this->_invalidCache = null;

            return $value[0];

        }

        else

        {

            $this->_invalidCache = $value[0];

            return false;

        }

    }


    /**

     * * @see \CCache::mget()

     *

     * @param array $ids

     *

     * @return array

     */

    public function mget($ids)

    {

        $uids = array();

        foreach ($ids as $id)

            $uids[$id] = $this->generateUniqueKey($id);


        $values = $this->getValues($uids);

        $results = array();

        if($this->serializer === false)

        {

            foreach ($uids as $id => $uid)

                $results[$id] = isset($values[$uid]) ? $values[$uid] : false;

        }

        else

        {

            $this->_invalidCache = array();

            foreach($uids as $id => $uid)

            {

                if(isset($values[$uid]))

                {

                    $value = $this->serializer === null ? unserialize($values[$uid]) : call_user_func($this->serializer[1], $values[$uid]);

                    if(is_array($value) && (!$value[1] instanceof \ICacheDependency || !$value[1]->getHasChanged()))

                    {

                        \Yii::trace('Serving "'.$id.'" from cache','system.caching.'.get_class($this));

                        $results[$id] = $value[0];

                    }

                    else

                    {

                        $this->_invalidCache[$id] = $value[0];

                    }


                }

            }

        }

        return $results;

    }


    /**

     * Returns data from invalidated cache. Suitable for using with dependencies.

     * @return mixed

     */

    public function getInvalidCache()

    {

        return $this->_invalidCache;

    }


    /**

     * Returns true if some dependencies were failed.

     *

     * @return bool|array

     */

    public function getHasInvalidCache()

    {

        return !empty($this->_invalidCache);

    }


    /**

     * Public analog of \CMemCache::setValue() with generateUniqueKey()

     *

     * @param string  $key

     * @param mixed   $value

     * @param integer $expire

     *

     * @return bool

     */

    public function setValueDirect($key, $value, $expire = 0)

    {

        $key = $this->generateUniqueKey($key);

        return $this->useMemcached ? $this->getMemCache()->set($key,$value,$expire) : $this->$this->getMemCache()->set($key,$value,0,$expire);

    }


    /**

     * @see \CMemCache::addValue()

     *

     * @param string $key

     * @param mixed $value

     * @param int $expire

     *

     * @return bool

     */

    public function addValueDirect($key,$value,$expire = 0)

    {

        $key = $this->generateUniqueKey($key);

        return $this->useMemcached ? $this->getMemCache()->add($key,$value,$expire) : $this->getMemCache()->add($key,$value,0,$expire);

    }


    /**

     * @see \CMemCache::getValue()

     *

     * @param string $key

     *

     * @return array|string

     */

    public function getValueDirect($key)

    {

        $key = $this->generateUniqueKey($key);

        return $this->getMemCache()->get($key);

    }


    /**

     * Direct multiple get function for memcache(d)

     *

     * @param $ids

     *

     * @return array

     */

    public function getValuesDirect($ids)

    {

        $uids = array();


        foreach ($ids as $id)

            $uids[$id] = $this->generateUniqueKey($id);


        return $this->getValues($uids);

    }


    public function increment($key)

    {

        $key = $this->generateUniqueKey($key);

        $this->getMemCache()->increment($key);

    }


    /**

     * Увиличить версию тега

     * @param $aTag

     */

    public function updateTag($aTag)

    {

        $key = $this->generateUniqueKey($aTag);

        $this->getMemCache()->increment($key);

    }


    /**

     * Увеличить версию перечисленных тегов

     * @param array $aTags

     */

    public function updateTags(array $aTags)

    {

        foreach($aTags as $tag)

        {

            $key = $this->generateUniqueKey($tag);

            $this->getMemCache()->increment($key);

        }

    }


    /**

     * @param string $key a key identifying a value to be cached

     * @return string a key generated from the provided key which ensures the uniqueness across applications

     */

    public function generateUniqueKey($key)

    {

        return $this->hashKey ? md5($this->keyPrefix.$key) : $this->keyPrefix.$key;

    }

}



TaggedCacheDependency




<?php


namespace application\components\cache;


/**

 * Клас зависимостей для унификации работы с кешем

 *

 * @link   https://neval.co.ua/svn/turbosmsyii/trunk/protected/components/cache/CacheDependency.php

 * @author lexand

 */

class TaggedCacheDependency implements \ICacheDependency

{

    public $cacheID = 'cache';


    /**

     * @var array

     */

    private $_tagsNames;


    /**

     * @var array

     */

    private $_tags;


    public function __construct(array $aTags)

    {

        $this->_tagsNames = $aTags;

    }


    public function evaluateDependency()

    {

        $cache = $this->getCache();

        foreach ($this->_tagsNames as $tag)

        {

            $key = $cache->generateUniqueKey($tag);

            $this->_tags[$key] = (function_exists('mt_rand') ? mt_rand(0, mt_getrandmax()) : rand(0, getrandmax()));

            $cache->setValueDirect($tag, $this->_tags[$key], 0);

        }

    }


    public function getHasChanged()

    {

        $cache = $this->getCache();

        $tags = $cache->getValuesDirect($this->_tagsNames);

        foreach ($this->_tagsNames as $tag)

        {

            $key = $cache->generateUniqueKey($tag);

            if ($this->_tags[$key] != $tags[$key])

            {

                return true;

            }

        }

        return false;

    }


    /**

     * Получаем компонент кеша

     * @return \CCache

     */

    private function getCache()

    {

        $cache = \Yii::app()->getComponent($this->cacheID);

        if (empty($cache))

        {

            throw new \CException(__CLASS__ . ': cache components has not been initialized');

        }


        return $cache;

    }


}




[/spoiler]