Memcached community предприняло немало попыток написать «родные» патчи для кода memcached, добавляющие в него поддержку тэгов. Наиболее известный из таких патчей — проект memcached-tag. К сожалению, memcached-tag все еще очень далек от стабильной версии: нетрудно написать скрипт, приводящий к зависанию пропатченного memcached-сервера. Похоже, на момент написания данной статьи не существует ни одного надежного решения проблемы тэгирования на уровне самого memcached-сервера.
Dklab_Cache — это (в основном) библиотека поддержки тэгирования ключей для memcached, использующая интерфейсы Zend Framework. Сама библиотека написана на чистом PHP. Вот полный список возможностей библиотеки:
TagEmuWrapper реализует стандартный backend-интерфейс Zend_Cache_Backend_Interface, поэтому с точки зрения вызывающей системы он сам является кэш-backend'ом. Вообще, Zend Framework хорош тем, что на уровне интерфейса он поддерживает тэги с самого начала! Например, в методе save() уже имеется параметр, позволяющий снабдить ключ тэгами. Однако ни один из backend-ов в составе Zend Framework тэги не поддерживает: попытка добавить тэг к некоторому ключу вызывает исключение (в частности, для Zend_Cache_Backend_Memcached).
Технические подробности, документацию, а также примеры использования можно посмотреть тут: dklab.ru/lib/Dklab_Cache
Работа с типичной кэширующей системой (в том числе с memcached) заключается в выполнении трех основных операций:
К сожалению, в чистом виде этот подход удается применять не так часто. Дело в том, что данные в БД могут измениться, и мы должны каким-то образом очистить ячейку кэша, чтобы пользователь увидел результаты этих изменений немедленно. Можно использовать метод remove() с указанием ключа, однако во многих случаях в момент обновления данных мы просто не знаем, в каких именно ячейках они кэшируются.
Проблема, на самом деле, гораздо сложнее. В высоконагруженных системах данные добавляются в таблицы по нескольку (сотен) раз в секунду. Поэтому логика отслеживания зависимостей и проверки, какие ячейки кэша нужно очищать, а какие — нет, становится крайне сложной (а то и вовсе нереализуемой).
Тэгирование предоставляет решение этой проблемы. Каждый раз, когда данные записываютя в некоторую ячейку кэша, мы помечаем их тэгами — пометками, представляющими зависимости этих данных от других частей системы. Тэги как бы позволяют объединять ячейки в множественные пересекающиеся группы. В дальнейшем мы можем дать команду «очистить все ячейки, помеченные определенным тэгом».
Давайте модифицируем предыдущий пример с использованием тэгов. Предположим, что SQL-запрос существенно зависит от ID текущего пользователя $loggerUserId, поэтому каждому такому пользователю выделяется отдельная ячейка с именем «key_{$loggedUserId}». Однако данные зависят и от ID другого человека $ownerUserId, чей профиль просматривает текущий пользователь. В этом случае мы можем пометить ячейку тэгом, связанным с пользователем $ownerUserId:
Теперь, если меняются данные в профиле пользователя $ownerUserId (например, человек поменял свое имя), нам достаточно дать команду на очистку тэга, связанного с этим профилем:
Обратите внимание, что кэш-ячейки всех остальных пользователей при этом не пострадают: очистятся только те, которые зависели от $ownerUserId.
Собственно, фраза «пометить ячейку C тэгом T» означает то же, что утверждение «ячейка C зависит от данных, описанных как T». Тэги — это зависимости, ничего более.
Прежде, чем продолжать рассказ о тэгах, давайте вернемся немного назад и поговорим о более общей концепции — о зависимостях. Что это за зависимости? В типичном случае (даже без использовании тэгов) нам приходится несколько раз ссылаться за ключ кэширования, чтобы эффективно работать с данными:
и потом еще в совершенно другой части программы:
Как видите, фразу «profile_{$userId}» приходится повторять аж три раза. И если в первом случае мы можем убрать повтор ценой введения новой переменной:
… то во второй части программы нам «в лоб» не избавиться от знания, как именно строится ключ кэширования, и от каких параметров он зависит.
Важное замечание
Строчка «profile_{$userId}» — это именно знание, и не следует недооценивать вред о распространении этого знания по излишне большому числу независимых мест. В нашем примере знание очень просто, но на практике ключ кэша может зависеть от десятков различных параметров, часть из которых нужно даже загружать из БД по первому требованию.
Ситуация в действительности даже хуже, чем может показаться.
Вместо долгих разъяснений я сразу приведу пример использования Slot-класса, построенного в соответствии с идеологией Dklab_Cache_Frontend.
Для очистки кэша:
Чем же это лучше?
Слоты, помимо прочего, поддерживают тэгирование. Вот пример использования тэгов для сквозного кэширования (естественно, можно применять и «несквозное»).
Вы должны создать столько классов-тэгов, сколько различных видов зависимостей существует в вашей системе. Классы-тэги особенно удобны, когда приходит пора очищать некоторые тэги:
Как видите, знание о зависимостях тэгов снова хранится в единственном месте. Вы теперь просто не сможете случайно «промахнуться» и очистить не тот тэг: система выдаст ошибку либо о несуществующем классе, либ о неверном типе параметра конструктора.
В этой статье говорится сразу обо всем: и о тэгировании кэша, и о кэш-зависимостях в коде, и о методе абстракции от кэш-хранилища Slot и Tag, реализованном в библиотеке.
Скачать исходники библиотеки и примеры можно здесь: dklab.ru/lib/Dklab_Cache
Библиотека 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_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.
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 — типизированный. Это означает, что мы не сможем «подсунуть» слот-классу что-либо, кроме корректного объекта-польователя. Естественно, зависимость может быть от нескольких объектов; все это определяется параметрами конструктора.
Ну а теперь, собственно, о тэгах
Слоты, помимо прочего, поддерживают тэгирование. Вот пример использования тэгов для сквозного кэширования (естественно, можно применять и «несквозное»).
$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