В проекте, где я являюсь разработчиком, используется кеш. Сразу хочу оговориться, проект высоконагруженный, порядка двух тысяч человек в сутки. Удобным решением снять нагрузку с базы данных стало использование мемкеша. Поскольку проект на Zend Framework'е реализацию работы кеша соответственно взяли его. Но как выяснилось не самая удачная реализация, поскольку полностью отсутствует работа с тегами, это не дает нам возможности чистить кеш выборочно.
Каким образом можно реализовать выборочную очистку кеша? Все очень просто, создаем контейнер в который будут записываться имена тегов, они будут ключами для других контейнеров, в них будут храниться наши хеши айдишников кешируемых методов. Для кеш фронтенда был взят Zend_Cache_Frontend_Class. Именно он генерирует хеши ключей данных кешируемых методов.
Выглядит это следующим образом:
Особо не вдаваясь в описание реализации предлагаю готовое решение.
Создаем свой файл бэкенда My_Cache_Backend_Memcached
Как пользоваться?
Привожу код примера, предупреждаю настройки бекенда и фронтенда передавать можно другим способом, приведенный код только для того чтобы понять как пользоваться.
Вместо стандартного бэкенда «Memcached» необходимо передать свой класс бэкенда «My_Cache_Backend_Memcached», также необходимо указать $customBackendNaming = true это 6 параметр в вызове фабрики Zend_Cache::factory.
Default_Models_MemcacheData1 и Default_Models_MemcacheData2 это наши кешируемые классы, они полностью идентичны. Привожу пример одного из них:
Как видно из кода при каждом вызове метода cachedMethod мы дожны получать рандомное значение.
При выполнении кода мы получим нечто похожее на следующее:
Проверить кеш в действии можно выполнив следующий код
Ситуация поменяется, мы получим приблизительно следующие данные:
В последующем ситуация не измениться.
Для очистки кеша воспользуемся следующей кодом
Убедитесь что кеш чиститься согласно передаваемого тега
Проблема выборочной очистки кеша была решена, произошел прирост производительности.
* спасибо homm за замечания. Теперь время жизни для тегов вычисляется исходя из настроек или если передаваемый параметр $specificLifetime больше, то берется он. Также добавлена проверка на изменение общего контейнера, в случае если теги изменялись.
p.s. Хочу предупредить php разработчиков, использовать этот код вы можете на свой собственный страх и риск. Данный код может являться панацеей для частных случаев. И не является взрывоопасным.
Используя данный код помните, что имена тегов также являются ключами, как и сами передаваемые id.
Каким образом можно реализовать выборочную очистку кеша? Все очень просто, создаем контейнер в который будут записываться имена тегов, они будут ключами для других контейнеров, в них будут храниться наши хеши айдишников кешируемых методов. Для кеш фронтенда был взят Zend_Cache_Frontend_Class. Именно он генерирует хеши ключей данных кешируемых методов.
Выглядит это следующим образом:
Tags_Container – это имя глобального контейнера, зададим его константой.
MemcacheTag1, MemcacheTag2 и MemcacheTagN – это имена сохраняемых тегов, которые будут находиться в Tags_Container в виде массива, они же будут являться контейнерами для хешируемых айдишником.
Особо не вдаваясь в описание реализации предлагаю готовое решение.
Создаем свой файл бэкенда My_Cache_Backend_Memcached
class My_Cache_Backend_Memcached extends Zend_Cache_Backend_Memcached
{
/**
* @const string
*/
const TAGS_CONTAINER_NAME = 'Tags_Container';
/**
* @return array
*/
protected function _getTagsContainer()
{
$tagsContainer = $this->load(self::TAGS_CONTAINER_NAME);
if (false === $tagsContainer) {
$tagsContainer = array();
}
if (is_string($tagsContainer)) {
$tagsContainer = array($tagsContainer);
}
return $tagsContainer;
}
/**
* @param $tagName
*
* @return array
*/
protected function _getIdsByTag($tagName)
{
$tagIds = $this->load($tagName);
if (false === $tagIds) {
$tagIds = array();
}
if (is_string($tagIds)) {
$tagIds = array($tagIds);
}
return $tagIds;
}
/**
* Save some string datas into a cache record
*
* Note : $data is always "string" (serialization is done by the
* core not by the backend)
*
* @param string $data Datas to cache
* @param string $id Cache id
* @param array $tags Array of strings, the cache record will be tagged
* by each string entry
* @param bool|int $specificLifetime If != false, set a specific lifetime
* for this cache record (null => infinite lifetime)
* @return bool True if no problem
*/
public function save($data, $id, $tags = array(), $specificLifetime = false)
{
$lifetime = $this->getLifetime($specificLifetime);
$tagsLifetime = $this->getLifetime(false);
if ($lifetime > $tagsLifetime) {
$tagsLifetime = $lifetime;
}
if ($this->_options['compression']) {
$flag = MEMCACHE_COMPRESSED;
} else {
$flag = 0;
}
$result = true;
if (count($tags) > 0) {
$tagsContainer = $this->_getTagsContainer();
$containerChanged = false;
foreach($tags as $tagName) {
if ($tagName == self::TAGS_CONTAINER_NAME) {
Zend_Cache::throwException('Incorrect name tag "' . $tagName . '"');
}
if (in_array($id, $tagsContainer)) {
Zend_Cache::throwException('The key with id = "' . $id . '" already used in the tags');
}
if (!in_array($tagName, $tagsContainer)) {
$containerChanged = true;
$tagsContainer[] = $tagName;
}
$tagIds = $this->_getIdsByTag($tagName);
if (!in_array($id, $tagIds)) {
$tagIds[] = $id;
}
$result = $result && @$this->_memcache->set(
$tagName, array($tagIds), $flag, $tagsLifetime
);
}
if ($containerChanged) {
$result = $result && @$this->_memcache->set(
self::TAGS_CONTAINER_NAME,
array($tagsContainer), $flag, $tagsLifetime
);
}
}
// ZF-8856: using set because add needs a second request if item already exists
$result = $result && @$this->_memcache->set(
$id, array($data, time(), $lifetime), $flag, $lifetime
);
return $result;
}
/**
* @param string $mode
* @param array $tags
*
* @return array
*/
protected function _get($mode, $tags = array())
{
if (is_string($tags)) {
$tags = array($tags);
}
$tagNames = $this->_getTagsContainer();
switch($mode) {
case 'ids':
break;
case 'tags':
$tagNames = array_intersect($tagNames, $tags);
break;
case 'matching':
$tagNames = array_intersect($tagNames, $tags);
break;
case 'notMatching':
$tagNames = array_diff($tagNames, $tags);
break;
default:
Zend_Cache::throwException('Invalid mode for _get() method');
break;
}
$ids = array();
foreach($tagNames as $tagName) {
$ids = array_merge($this->_getIdsByTag($tagName), $ids);
}
return $ids;
}
/**
* Return an array of stored cache ids
*
* @return array
*/
public function getIds()
{
return $this->_get('ids', array());
}
/**
* Return an array of stored tags
*
* @return array
*/
public function getTags()
{
return $this->_get('tags', array());
}
/**
* Return an array of stored cache ids which match given tags
*
* In case of multiple tags, a logical AND is made between tags
*
* @param array $tags array of tags
*
* @return array
*/
public function getIdsMatchingTags($tags = array())
{
return $this->_get('matching', $tags);
}
/**
* Return an array of stored cache ids which don't match given tags
*
* In case of multiple tags, a logical OR is made between tags
*
* @param array $tags array of tags
*
* @return array
*/
public function getIdsNotMatchingTags($tags = array())
{
return $this->_get('notMatching', $tags);
}
/**
* Return an associative array of capabilities (booleans) of the backend
*
* @return array associative of with capabilities
*/
public function getCapabilities()
{
$capabilities = parent::getCapabilities();
$capabilities['tags'] = true;
return $capabilities;
}
/**
* @param string $mode
* @param array $tags
*
* @return bool
*/
protected function _clean($mode, $tags)
{
$result = false;
switch ($mode) {
case Zend_Cache::CLEANING_MODE_ALL:
$result = $this->_memcache->flush();
break;
case Zend_Cache::CLEANING_MODE_MATCHING_TAG:
$ids = $this->getIdsMatchingTags($tags);
$result = true;
foreach($ids as $id) {
$result = $result && $this->remove($id);
}
break;
case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
$ids = $this->getIdsNotMatchingTags($tags);
$result = true;
foreach($ids as $id) {
$result = $result && $this->remove($id);
}
break;
case Zend_Cache::CLEANING_MODE_OLD:
case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
$this->_log(self::TAGS_UNSUPPORTED_BY_CLEAN_OF_MEMCACHED_BACKEND);
break;
default:
Zend_Cache::throwException('Invalid mode for clean() method');
break;
}
return $result;
}
/**
* @param string $mode
* @param array $tags
*
* @return mixed
*/
public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
{
return $this->_clean($mode, $tags);
}
}
Как пользоваться?
Привожу код примера, предупреждаю настройки бекенда и фронтенда передавать можно другим способом, приведенный код только для того чтобы понять как пользоваться.
$frontendName = 'Class';
$backendName = 'My_Cache_Backend_Memcached';
$frontendOptions = array();
$memcacheDataObject1 = new Default_Models_MemcacheData1;
$memcacheDataObject2 = new Default_Models_MemcacheData2;
$backendOptions = array(
'servers' => array(
array(
'host' => '127.0.0.1',
'port' => '11211',
'persistent' => 1,
'weight' => 5,
'timeout' => 5,
'retry_interval' => 15
)
)
);
$frontendOptions['cached_entity'] = $memcacheDataObject1;
$cachedObject1 = Zend_Cache::factory(
$frontendName,
$backendName,
$frontendOptions,
$backendOptions,
false,
true
);
$cachedObject1->setTagsArray(array('Memcached_Tag1'));
$frontendOptions['cached_entity'] = $memcacheDataObject2;
$cachedObject2 = Zend_Cache::factory(
$frontendName,
$backendName,
$frontendOptions,
$backendOptions,
false,
true
);
$cachedObject2->setTagsArray(array('Memcached_Tag2'));
Вместо стандартного бэкенда «Memcached» необходимо передать свой класс бэкенда «My_Cache_Backend_Memcached», также необходимо указать $customBackendNaming = true это 6 параметр в вызове фабрики Zend_Cache::factory.
Default_Models_MemcacheData1 и Default_Models_MemcacheData2 это наши кешируемые классы, они полностью идентичны. Привожу пример одного из них:
class Default_Models_MemcacheData1
{
public function cachedMethod()
{
return rand(111, 999);
}
}
Как видно из кода при каждом вызове метода cachedMethod мы дожны получать рандомное значение.
for ($i = 0; $i < 3; $i++)
{
Zend_Debug::dump($memcacheDataObject1->cachedMethod(), 'cached data:');
}
for ($i = 0; $i < 3; $i++)
{
Zend_Debug::dump($memcacheDataObject2->cachedMethod(), 'cached data:');
}
При выполнении кода мы получим нечто похожее на следующее:
cached data: int(468)
cached data: int(676)
сached data: int(721)
сached data: int(182)
cached data: int(414)
cached data: int(561)
Проверить кеш в действии можно выполнив следующий код
for ($i = 0; $i < 3; $i++)
{
Zend_Debug::dump($cachedObject1->cachedMethod(), 'cached data:');
}
for ($i = 0; $i < 3; $i++)
{
Zend_Debug::dump($cachedObject2->cachedMethod(), 'cached data:');
}
Ситуация поменяется, мы получим приблизительно следующие данные:
cached data: int(901)
cached data: int(901)
cached data: int(901)
cached data: int(865)
cached data: int(865)
cached data: int(865)
В последующем ситуация не измениться.
Для очистки кеша воспользуемся следующей кодом
$cachedClass1->clean(
Zend_Cache::CLEANING_MODE_MATCHING_TAG,
array('Memcached_Tag1')
);
Убедитесь что кеш чиститься согласно передаваемого тега
Проблема выборочной очистки кеша была решена, произошел прирост производительности.
* спасибо homm за замечания. Теперь время жизни для тегов вычисляется исходя из настроек или если передаваемый параметр $specificLifetime больше, то берется он. Также добавлена проверка на изменение общего контейнера, в случае если теги изменялись.
p.s. Хочу предупредить php разработчиков, использовать этот код вы можете на свой собственный страх и риск. Данный код может являться панацеей для частных случаев. И не является взрывоопасным.
Используя данный код помните, что имена тегов также являются ключами, как и сами передаваемые id.