2008-04-19
Обсудить на форуме
Dklab_Cache это (в основном) библиотека поддержки тэгирования ключей для memcached, использующая интерфейсы Zend Framework. Вот полный список возможностей библиотеки:
- Backend_TagEmuWrapper: тэги для memcached и любых других backend-систем кэширования Zend Framework;
- Backend_NamespaceWrapper: поддержка пространств имен для memcached и др.;
- Backend_Profiler: подсчет статистики по использованию memcached и др. backend-ов;
- Backend_MemcachedTag: поддержка еще весьма "сырого" патча memcached-tag;
- Frontend_Slot, Frontent_Tag: каркас для высокоуровневого построения систем кэшиирования в сложных проектах.
Поддержка со стороны backend: Dklab_Cache_Backend
Memcached отличная система кэширования данных, особенно удобная при разработке высоконагруженных сайтов. Она используется в таких проектах, как Мой Круг, Facebook, LiveJournal и др. К сожалению, memcached не поддеживает тэгирование ключей, а потому код, следящий за валидностью кэша, подчас становится очень сложным.
|
Если в вашем проекте Zend Framework не используется (в том числе по принциальным соображениям), не расстраивайтесь. Совершенно не обязательно подключать весь Zend Framework, чтобы работать с его подсистемой кэширования. Вы можете взять только несколько необходимых файлов; они, в частности, содержатся в дистрибутиве данной статьи. Конечно, вам следует вначале прочитать документацию по кэшированию в Zend Framework на русском языке>.
|
Нужно заметить, что memcached community предприняло немало попыток написать "родные" патчи для кода memcached, добавляющие в него поддержку тэгов. Наиболее известный из таких патчей проект memcached-tag. К сожалению, memcached-tag все еще очень далек от стабильной версии: нетрудно написать скрипт, приводящий к зависанию пропатченного memcached-сервера. Увы, на момент написания данной статьи не существует ни одного надежного решения проблемы тэгирования на уровне самого memcached-сервера.
Поддержка тэгов: Dklab_Cache_Backend_TagEmuWrapper
Класс 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).
|
Пример использования библиотеки приведен ниже.
Листинг 1: Использование обертки Dklab_Cache_Backend_TagEmuWrapper
|
// Initialize the standard memcached backend.
$memcached = new Zend_Cache_Backend_Memcached(
array("servers" => array("host" => "localhost"))
);
// Create the wrapper object with tags support for $memcached.
$backend = new Dklab_Cache_Backend_TagEmuWrapper($memcached);
// Save two keys with a number of tags.
$backend->save("Ivan", "firstname", array("tag1", "tag2", "tag3"));
$backend->save("Ivanov", "lastname", array("tag3", "tag4"));
// Clear all keys marked by the tag "tag3".
$backend->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array("tag3"));
// Load a key: returns false, because the key was cleared by tag.
var_dump($backend->load("firstname")); |
Вы также можете использовать TagEmuWrapper наравне с другими backend-классами Zend Framework в его frontend-классах кэширования:
Листинг 2: Совместное использование с frontend-классом Output
|
$backend = new Dklab_Cache_Backend_TagEmuWrapper($memcached);
$frontend = new Zend_Cache_Frontend_Output();
$frontend->setBackend($backend);
if (!$frontend->start()) {
echo "Some page content!";
$frontend->end(array("tag1", "tag2"));
} |
Аспекты производительности
Т.к. memcached не работает с тэгами на встроенном уровне, библиотека TagEmuWrapper эмулирует их поддержку, заводя для каждого тэга специальный ключ в оборачиваемом backend-е. Это ведет к увеличению накладных расходов на кэширование. К примеру, если вы записываете в кэш ключ и помечаете его 10 разными тэгами, библиотека фактически произведет 11 обращений к memcached-серверу на запись. Более того, если вы затем захотите считать значение этого ключа, последуют те же самые 11 запросов для проверки "валидности" всех связанных с ним тэгов. Таким образом, каждый доболнительный тэг добавляет одну операцию при чтении и одну операцию при записи ключа.
В большинстве систем данные накладные расходы не составляют серьезных проблем, т.к. memcached работает очень быстро. Однако вам все равно стоит помнить о них и не помечать ключи чрезмерным количеством тэгов без необходимости.
Пространства имен: Dklab_Cache_Backend_NamespaceWrapper
Класс-обертка NamespaceWrapper позволяет разбить единственный backend-сервер на несколько непересекающихся пространств имен, содержащих гарантированно непересекающиеся наборы ключей. Каждое пространство имен для системы выглядит, как отдельный backend-объект.
Зачем нужны пространства имен? Иногда разные части проекта используют один и тот же кэш-backend совершенно различным образом. Например, какие-то ключи в кэше хранятся "вечно", а какие-то "стираются" при любом изменении таблицы message в БД.
Листинг 3: Использование пространств имен
|
// Initialize the standard memcached backend.
$memcached = new Zend_Cache_Backend_Memcached(
array("servers" => array("host" => "localhost"))
);
// Create the global namespace.
$globalBackend = new Dklab_Cache_Backend_NamespaceWrapper($memcached, "global");
// Get last-modified time of "message" table.
$lastModified = getLastModifiedTimeOfTable("message");
// Create the "per-table" namespace based on the table time.
$messageBackend = new Dklab_Cache_Backend_NamespaceWrapper($memcached, "message_{$lastModified}");
// Read keys.
$val1 = $globalBackend->load("key1");
$string = $messageBackend->load("hello"); |
Теперь мы можем быть уверены, что при любом изменении таблицы message все кэши, связанные с ней, "очистятся": ведь мы используем пространство имен, привязанное ко времени последнего обновления таблицы. Данная схема особенно хорошо работает, когда таблица message обновляется редко (например, содержит текстовые строки локализации сайта).
|
В действительности, конечно, ключи не очищаются. Они формально остаются в памяти memcached, однако доступ к ним более невозможен, т.к. сменилось название пространства имен. Такие ключи будут в скором времени вытеснены за пределы кэша самим memcached, который удаляет давно неиспользуемые записи автоматически.
|
Измерение накладных расходов: Dklab_Cache_Backend_Profiler
Класс-обертка Profiler позволяет измерить время, которое потратили скрипты на "общение" с memcached-сервером. С его помощью вы легко сможете оценить, является ли memcached "узким местом" в вашем проекте или нет.
Класс Profiler вызывает некоторую callback-функцию или метод всякий раз, когда производится обращение к "обернутому" в него backend-у. Функции передается время, потраченное в этом backend-е, а также имя вызванного метода (load, save, test и т. д.).
Листинг 4: Измерение накладных расходов
|
// Initialize the standard memcached backend.
$memcached = new Zend_Cache_Backend_Memcached(
array("servers" => array("host" => "localhost"))
);
// Create the wrapped backend with statistics accounting.
$backend = new Dklab_Cache_Backend_Profiler($memcached, "profilerCallback");
// Do some operations with $backend.
$backend->save("Some data", "key");
echo $backend->load("key");
// Print usage statistics.
printf("Took %.3f ms; performed %d queries.\n", $mmcTime * 1000, $mmcCount);
// Define the profiler accounting callback.
function profilerCallback($time, $method)
{
global $mmcTime, $mmcCount;
$mmcTime += $time;
$mmcCount += 1;
} |
Конечно, вместо callback-функции можно передавать и ссылку на метод некоторого объекта; это делается стандартным для PHP способом:
Листинг 5: Передача ссылки на метод
|
// Create the wrapped backend with statistics accounting.
$backend = new Dklab_Cache_Backend_Profiler($memcached, array($obj, "callbackMethod")); |
Использование всех трех "оберток" разом
Паттерн декоратор ("обартка"), используемый в классах Dklab_Cache_Backend, позволяет делать занятную штуку: использовать одновременно все три обертки. Вот как это делается:
Листинг 6
|
// Create a "super-wrapped" backend.
$backend = new Dklab_Cache_Backend_TagEmuWrapper(
new Dklab_Cache_Backend_NamespaceWrapper(
new Dklab_Cache_Backend_Profiler(
new Zend_Cache_Backend_Memcached(
array("servers" => array("host" => "localhost"))
),
"profilerCallback"
),
"namespaceName"
)
); |
Обратите внимание, что Profiler-декоратор должна быть самой внутренней, чтобы измерять время обращения к memcached-серверу, не искаженное накладными расходами на вызов остальных оберток. Но вообще, вы можете использовать декораторы в том порядке, который вам больше нравится.
"Родные" тэги: Dklab_Cache_Backend_MemcachedTag
К сожалению, "родной" патч для сервера memcached, memcached-tag, все еще далек от совершенства. Тем не менее, если вдруг в будущем он станет более стабильным, вы можете использовать класс Dklab_Cache_Backend_MemcachedTag, входящий в библиотеку Dklab_Cache. Если бы не плохая стабильность memcached-tag, то использование этого класса дало бы программе значительно лучшую производительность, чем TagEmuWrapper, т.к. в memcached-tag поддержка тэгов встроена в сам сервер memcached.
Поддержка со стороны frontend: Dklab_Cache_Frontend
Использовать только backend-классы библиотеки не очень удобно, потому что они предоставляют слишком низкоуровневый интерфейс к кэшу, а также провоцируют программиста на введение лишних зависимостей в коде. Все это подробно рассмотрено в статье Правильный способ кэширования данных (рекомендуем прочитать ее прямо сейчас, т.к. там детально разъясняются примеры). Решение предоставляет frontend-часть библиотеки.
Классы-слоты: Dklab_Cache_Frontend_Slot
Для начала необходимо определить в программе классы, которые будут представлять ячейки кэша различных типов. Класс должны быть производным от Dklab_Cache_Frontend_Slot. Определить их можно, например, вот так:
Листинг 7: Определение классов-слотов для будущего использования
|
// Базовый класс для всех будущих пользовательских классов-слотов.
// Определяет, с каким backend-ом будет идти работа.
class Cache_Slot_Abstract extends Dklab_Cache_Frontend_Slot {
protected function _getBackend() {
// Код может быть любым, лишь бы он возвращал один и тот же backend.
return Zend_Registry::get('memcachedBackend');
}
}
// Пользовательский класс 1: кэширование профиля пользователя.
class Cache_Slot_UserProfile extends Cache_Slot_Abstract {
public function __construct(User $user) {
parent::__construct("profile_{$user->id}", 3600 * 24);
}
}
// Пользовательский класс 2: кэширование пользовательского фотоальбома.
class Cache_Slot_UserPhotos extends Cache_Slot_Abstract {
public function __construct(User $user) {
parent::__construct("photos_{$user->id}", 3600 * 24);
}
}
// ... |
Классов-слотов должно быть столько, сколько "различных видов кэширования" существует в вашей системе. Каждый из слотов должен иметь метод _getBackend(), который определяет, в каком кэш-хранилище хранятся данные этого слота. Чтобы не повторять код этого метода в каждом классе, мы вынесли его в базовый класс Cache_Slot_Abstract и унаследовали все слоты от него.
Вот как теперь мы можем использовать наши новые классы-слоты:
Листинг 8
|
// Код использования кэша.
$slot = new Cache_Slot_UserProfile($user);
if (false === ($data = $slot->load())) {
$data = $user->loadProfile();
$slot->addTag(new Cache_Tag_User($loggedUser);
$slot->addTag(new Cache_Tag_Language($currentLanguage);
$slot->save($data);
}
// Еще вариант ("сквозной" вызов метода loadProfile()).
$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(); |
|
Обратите внимание, что при создании слота в его конструкторе мы передаем типизированный объект. Это увеличивает надежность программы: если вдруг кто-то попытается создать слот, не имея для этого всех данных, на основе которых выбирается его имя, он получит ошибку, а не непредсказуемо работающую программу. Если это еще не до конца ясно, самое время прочитать статью Правильный способ кэширования данных.
|
Классы-тэги: Dklab_Cache_Frontend_Tag
Наверное, вы заметили, что в примере выше используются не только классы-слоты, но также и классы-тэги Cache_Tag_User и Cache_Tag_Language. Их, конечно, тоже нужно определить в вашей программе. Классов-тэгов должно существовать столько, сколько имеется зависимостей между данными в приложении:
Листинг 9: Определение классов-тэгов для будущего использования
|
// Базовый класс для всех будущих пользовательских классов-тэгов.
// Определяет, с каким backend-ом будет идти работа.
class Cache_Tag_Abstract extends Dklab_Cache_Frontend_Tag {
protected function getBackend() {
// Использовать Zend_Registry совсем не обязательно; код может быть любым,
// лишь бы он всегда возвращал один и тот же backend.
return Zend_Registry::get('memcachedBackend');
}
}
// Пользовательский класс 1: кэширование профиля пользователя.
class Cache_Tag_User extends Cache_Tag_Abstract {
public function __construct(User $user) {
parent::__construct("user_{$user->id}");
}
}
// Пользовательский класс 2: кэширование пользовательского фотоальбома.
class Cache_Tag_Language extends Cache_Tag_Abstract {
public function __construct(Language $language) {
parent::__construct("language_{$language>id}");
}
}
// ... |
Если вы все сделали правильно и определили каждый класс-слот в отдельном файле, то в директории Cache/Slot у вас окажется аккуратный набор файлов-слотов, используемых в системе. Вам будет довольно легко в нем ориентироваться. Аналогично произойдет и для тэгов в директории Cache/Tag.
Полная картина классов
Неужели вы еще здесь? Отлично! Тогда вот полный список файлов с классами, затронутых в этой статье.
Листинг 10: Все файлы, затронутые в статье
|
lib/ - директория со всеми сайтонезависимыми библиотеками
Zend/ - стандартный Zend Framework или его часть Zend_Cache
...
Dklab/
Cache/ - дистрибутив библиотеки Dklab_Cache
Backend/ - backend-часть
TagEmuWrapper.php
NamespaceWrapper.php
Profiler.php
Frontend/ - frontend-часть
Slot.php
Tag.php
classes/ - директория классов, специфичных для конкретного проекта
Cache/ - спекифичные для проекта классы кэширования
Slot/ - слоты кэша
Abstract.php - абстрактный класс; все слоты унаследованы от него
UserProfile.php - слот кэширования профиля пользователя
UserPhotos.php - слот кэширования фотоальбома
... - другие слоты (может быть и сотня файлов)
Tag/ - тэги кэша
Abstract.php - абстрактный класс; все тэги унаследованы от него
User.php - зависимость от данных пользователя
Language.php - зависимость от текущего национального языка
... - другие тэги (может быть много) |
Резюме
Для полного понимания материала данной статьи, возможно, придется прочитать еще две:
Популярная система кэширования memcached не имеет "на борту" встроенной поддержки тэгов, хотя необходимость в ней остро ощущается в современных высоконагруженных проектах. Представленное в этой статье семейство PHP5-библиотек Dklab_Cache:
- добавляет поддержку тэгов к memcached или любому другому backend-у кэширования;
- поддерживает разбиение backend-кэша на несколько пространств имен;
- позволяет измерять время, затраченное на общение с memcached-сервером (либо любым другим backend-ом).
Также дистрибутив включает в себя и frontend-часть, облегчающую работу с кэшем в конечных приложениях:
- класс поддержки типизированный слотов кэша;
- центрадизованная поддержка типизированных тэгов.
Ну и последнее. Если примеры в этой статье покажутся вам недостаточно прозрачными, загляните в директорию t/ дистрибутива библиотеки. Там вы найдете более 20 regression-тестов, хранящихся в phpt-файлах. Изучение этих тесов хороший способ окончательно разобраться с функциональностью Dklab_Cache на примерах. |
|
|