Тегирование кеша в Yii Framework — это не больно

17 апреля 2011 г.

Однажды, разрабатывая один проект на 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’ом для того, что бы гарантировать, что этот тег проживет дольше самих записей. И хотя существует теоретическая вероятность, что тег «протухнет» раньше данных, на практике это крайне мало вероятно, т.к. очевидно, что теги инвалидации будут меньше и дергаться чаще самих данных.

Теги: рубрика PHP