Pull to refresh

Dklab_Cache: тэги в memcached, namespaces, статистика

PHP *
Memcached community предприняло немало попыток написать «родные» патчи для кода memcached, добавляющие в него поддержку тэгов. Наиболее известный из таких патчей — проект memcached-tag. К сожалению, memcached-tag все еще очень далек от стабильной версии: нетрудно написать скрипт, приводящий к зависанию пропатченного memcached-сервера. Похоже, на момент написания данной статьи не существует ни одного надежного решения проблемы тэгирования на уровне самого memcached-сервера.

Библиотека Dklab_Cache


Dklab_Cache — это (в основном) библиотека поддержки тэгирования ключей для memcached, использующая интерфейсы Zend Framework. Сама библиотека написана на чистом PHP. Вот полный список возможностей библиотеки:
  • Backend_TagEmuWrapper: тэги для memcached и любых других backend-систем кэширования Zend Framework;
  • Backend_NamespaceWrapper: поддержка пространств имен для memcached и др.;
  • Backend_Profiler: подсчет статистики по использованию memcached и др. backend-ов;
  • Frontend_Slot, Frontent_Tag: каркас для высокоуровневого построения систем кэшиирования в сложных проектах.
Собственно, для поддержки тэгов имеется класс TagEmuWrapper. Он представляет собой декоратор («обертку») для backend-классов кэширования Zend Framework. Другими словами, вы можете с его помощью «прозрачно» добавить поддержку тэгов в любую подсистему кэширования Zend Framework. Мы будем рассматривать backend для работы с memcached: Zend_Cache_Backend_Memcached, но, если в вашем проекте используется какой-то другой backend-класс, вы можете подключить тэгирование и к нему без каких-либо особенностей.

TagEmuWrapper реализует стандартный backend-интерфейс Zend_Cache_Backend_Interface, поэтому с точки зрения вызывающей системы он сам является кэш-backend'ом. Вообще, Zend Framework хорош тем, что на уровне интерфейса он поддерживает тэги с самого начала! Например, в методе save() уже имеется параметр, позволяющий снабдить ключ тэгами. Однако ни один из backend-ов в составе Zend Framework тэги не поддерживает: попытка добавить тэг к некоторому ключу вызывает исключение (в частности, для Zend_Cache_Backend_Memcached).

Технические подробности, документацию, а также примеры использования можно посмотреть тут: dklab.ru/lib/Dklab_Cache

Что такое тэги?


Работа с типичной кэширующей системой (в том числе с memcached) заключается в выполнении трех основных операций:
  • save($data, $id, $lifetime): сохранить данные $data в ячейке кэша с ключом $id. Можно указать «время жизни» ключа $lifetime; спустя это время данные в кэше «протухнут» и удалятся.
  • load($id): загрузить данные из ячейки с ключом $id. Если данные недоступны, возвращается false.
  • remove($id): очистить ячейку кэша с ключом $id.
Предположим, мы хотим эакэшировать долгий SQL-запрос для быстрого отображения части страницы. В этом случае мы проверяем: имеется ли запись в ячейке кэша, соответствующей этому запросу. Если ячейка пуста, данные загружаются из СУБД и сохраняются в кэш для возможных будущих извлечений.

if (false === ($data = $cache->load("key"))) {
    $data = executeHeavyQuery();
    $cache->save($data, "key");
}
display($data);

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

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

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

Давайте модифицируем предыдущий пример с использованием тэгов. Предположим, что SQL-запрос существенно зависит от ID текущего пользователя $loggerUserId, поэтому каждому такому пользователю выделяется отдельная ячейка с именем «key_{$loggedUserId}». Однако данные зависят и от ID другого человека $ownerUserId, чей профиль просматривает текущий пользователь. В этом случае мы можем пометить ячейку тэгом, связанным с пользователем $ownerUserId:

if (false === ($data = $cache->load("key_{$loggedUserId}"))) {
    $data = loadProfileFor($loggedUserId, $ownerUserId);
    $cache->save($data, "key_{$loggedUserId}", array("profile_{$ownerUserId}");
}
display($data);

Теперь, если меняются данные в профиле пользователя $ownerUserId (например, человек поменял свое имя), нам достаточно дать команду на очистку тэга, связанного с этим профилем:

$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array("profile_{$ownerUserId}");

Обратите внимание, что кэш-ячейки всех остальных пользователей при этом не пострадают: очистятся только те, которые зависели от $ownerUserId.

Собственно, фраза «пометить ячейку C тэгом T» означает то же, что утверждение «ячейка C зависит от данных, описанных как T». Тэги — это зависимости, ничего более.

Небольшое отступление: о зависимостях в коде


Прежде, чем продолжать рассказ о тэгах, давайте вернемся немного назад и поговорим о более общей концепции — о зависимостях. Что это за зависимости? В типичном случае (даже без использовании тэгов) нам приходится несколько раз ссылаться за ключ кэширования, чтобы эффективно работать с данными:

if (false === ($data = $cache->load("profile_{$userId}"))) {
    $data = loadProfileOf($userId);
    $cache->save($data, "profile_{$userId}", array(), 3600 * 24); // кэширование на 24 часа
}
display($data);

и потом еще в совершенно другой части программы:

$cache->remove("profile_{$userId}");

Как видите, фразу «profile_{$userId}» приходится повторять аж три раза. И если в первом случае мы можем убрать повтор ценой введения новой переменной:

$cacheKey = "profile_{$userId}";
$cacheTime = Config::getInstance()->cacheTime->profile;
if (false === ($data = $cache->load($cacheKey))) {
    $data = loadProfileFor($userId);
    $cache->save($data, $cacheKey, array(), $cacheTime);
}
display($data);

… то во второй части программы нам «в лоб» не избавиться от знания, как именно строится ключ кэширования, и от каких параметров он зависит.

Важное замечание
Строчка «profile_{$userId}» — это именно знание, и не следует недооценивать вред о распространении этого знания по излишне большому числу независимых мест. В нашем примере знание очень просто, но на практике ключ кэша может зависеть от десятков различных параметров, часть из которых нужно даже загружать из БД по первому требованию.

Ситуация в действительности даже хуже, чем может показаться.
  • Кто может дать гарантию, что в переменной $userId хранится именно ID текущего пользователя, а не какой-нибудь мусор? А что, если кто-то попробует подставить туда неверные данные? Очевидно, что ключ кэша в действительности зависит не от ID пользователя, а от самого этого пользователя. Попытка использовать для генерации ключа что-либо, кроме объекта-пользователя, заведомо ошибочна, но в программе это ограничение явно не выражено.
  • Время кэширования мы должны хранить не прямо в коде, а где-то в конфигурации системы (см. предыдущий пример), чтобы его можно было менять, не трогая код. Это — еще одна зависимость от роли кэш-ячейки и строчки «profile».

Как это работает в Dklab_Cache


Вместо долгих разъяснений я сразу приведу пример использования Slot-класса, построенного в соответствии с идеологией Dklab_Cache_Frontend.

$slot = new Cache_Slot_UserProfile($user);
if (false === ($data = $slot->load())) {
    $data = $user->loadProfile();
    $slot->save($data);
}
display($data);

Для очистки кэша:

$slot = new Cache_Slot_UserProfile($user);
$slot->remove();

Чем же это лучше?
  • Знание об алгоритме построения ключа кэша заключено в едином месте — в классе Cache_Slot_UserProfile.
  • Там же заключено знание о времени жизни кэша. В нашем случае мы задали его явно, однако никто не мешает брать время жизни из параметра конфигурации, имя которого совпадает с именем слот-класса.
  • Параметр $user конструктора класса Cache_Slot_UserProfile — типизированный. Это означает, что мы не сможем «подсунуть» слот-классу что-либо, кроме корректного объекта-польователя. Естественно, зависимость может быть от нескольких объектов; все это определяется параметрами конструктора.
Вы должны написать столько собственных слот-классов, сколько видов кэш-хранилищ существует у вас в программе. Это дисциплинирует: заглянув в директорию Cache/Slot, вы сразу сможете увидеть, сколько именно различных кэшей используется в программе, а также — от чего они зависят.

Ну а теперь, собственно, о тэгах


Слоты, помимо прочего, поддерживают тэгирование. Вот пример использования тэгов для сквозного кэширования (естественно, можно применять и «несквозное»).

$slot = new Cache_Slot_UserProfile($user);
$slot->addTag(new Cache_Tag_User($loggedUser);
$slot->addTag(new Cache_Tag_Language($currentLanguage);
$data = $slot->thru($user)->loadProfile();
display($data);

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

$tag = new Cache_Tag_Language($currentLanguage);
$tag->clean();

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

Заключение


В этой статье говорится сразу обо всем: и о тэгировании кэша, и о кэш-зависимостях в коде, и о методе абстракции от кэш-хранилища Slot и Tag, реализованном в библиотеке.

Скачать исходники библиотеки и примеры можно здесь: dklab.ru/lib/Dklab_Cache
Tags:
Hubs:
Total votes 73: ↑59 and ↓14 +45
Views 4.3K
Comments Comments 57