Тегирование кеша в Yii Framework — это не больно
Однажды, разрабатывая один проект на Yii Framework, мне понадобился механизм тегов для кеша. Первое, что пришло мне в голову — это статья Дмитрия Котерова про реализацию тегов в Zend Framwork. Казалось бы, за чем дело-то стало? Бери алгоритм и один в один кодируй его для Yii. Но тут мне пришла мысль, а что если…
Оптимизация алгоритма
Основная идея оптимизации довольно проста — мы инвертируем логику таким образом, что бы кеш становился не валидным не при отсутствии записи в кеше, а при ее наличии.
Зачем нам для каждой записи в кеше создавать тут же добавлять еще по одной на каждый тег, если скорее всего большинство из них так никогда и не пригодятся? В место этого можно считать кеш валидным, если специальная запись (назовем ее, «тегом инвалидации») отсутствует и не валидным, если присутствует. Соответственно тогда весь алгоритм тегирования сильно упрощается:
- При добавлении записи в кеш мы просто добавляем к ней список тегов, а так же текущий timestamp
- При очистке кеша по тегу мы добавляем тег инвалидации, у которого в качестве значения будет так же текущий timestamp
- При получении записи мы так же запрашиваем все теги инвалидации и если они отсутствуют, либо имеют timestamp меньше, чем timestamp текущей записи, то данные валидны.
Реализация
Теперь приступим к самому интересному. Благодаря механизмам Behavior и Cache Dependency, в Yii реализация данного алгоритма займет едва-ли больше 50 строк :)
Behavior
Первое, что нам необходимо реализовать — это Behavior, который добавит метод очистки кеша по тегу.
class TaggingBehavior extends CBehavior { const PREFIX = '__tag__'; /** * Инвалидирует данные, помеченные тегом(ами) * * @param $tags * @return void */ public function clear($tags) { foreach ((array)$tags as $tag) { $this->owner->set(self::PREFIX.$tag, time()); } } }
Тут все просто, передаем массив тегов и для каждого из них добавляем тег инвалидации.
ICacheDependency
Второе, что нам необходимо написать — это класс реализующий интерфейс ICacheDependency. Объект именно этого класса мы будем передавать 4’ым параметром в метод CCache::set($id, $value, $expire, $dependency)
class Tags implements ICacheDependency { protected $timestamp; protected $tags; /** * В качестве параметров передается список тегов * * @params tag1, tag2, ..., tagN */ function __construct() { $this->tags = func_get_args(); } /** * Evaluates the dependency by generating and saving the data related with dependency. * This method is invoked by cache before writing data into it. */ public function evaluateDependency() { $this->timestamp = time(); } /** * @return boolean whether the dependency has changed. */ public function getHasChanged() { $tags = array_map(function($i) { return TaggingBehavior::PREFIX.$i; }, $this->tags); $values = Yii::app()->cache->mget($tags); foreach ($values as $value) { if ((integer)$value > $this->timestamp) { return true; } } return false; } }
В конструкторе мы передаем список тегов, делать с ними на данном этапе ничего не требуется.
Метод evaluateDependency(), как видно из комментария, вызывается непосредственно перед сохранением данных в кеше, в этот момент мы должны создать timestamp.
И, наконец, основная часть алгоритма заключается в методе getHasChanged(), который вызывается сразу после получения данных из кеша. Тут тоже все просто: получаем список тегов инвалидации и поочередно сравниваем их timestamp с timestamp’ом текущей записи. В отличии от ZF, Yii поддерживает multiget для memcache бэкенда (метод CCache::mget()), поэтому запрос к мемкешу делается один.
Пример
Пример использование тегов:
// Добавление записи с тегами teg1 и tag2 Yii::app()->cache->set($key, $value, 0, new Tags('tag1', 'tag2')); // Очистка кеша по тегу tag2 Yii::app()->cache->clear('tag2');
Вот и вся любовь :)
Данная реализация применима к любому бэкенду, достаточно подключить Behavior.
Проблемы
Основная проблема данного алгоритма заключается в том, что приходится ставить тег инвалидации с пожизненным expire’ом для того, что бы гарантировать, что этот тег проживет дольше самих записей. И хотя существует теоретическая вероятность, что тег «протухнет» раньше данных, на практике это крайне мало вероятно, т.к. очевидно, что теги инвалидации будут меньше и дергаться чаще самих данных.