Как стать автором
Обновить

Учим Zend Memcache работать с тегами

Время на прочтение6 мин
Количество просмотров2.6K
В проекте, где я являюсь разработчиком, используется кеш. Сразу хочу оговориться, проект высоконагруженный, порядка двух тысяч человек в сутки. Удобным решением снять нагрузку с базы данных стало использование мемкеша. Поскольку проект на Zend Framework'е реализацию работы кеша соответственно взяли его. Но как выяснилось не самая удачная реализация, поскольку полностью отсутствует работа с тегами, это не дает нам возможности чистить кеш выборочно.

Каким образом можно реализовать выборочную очистку кеша? Все очень просто, создаем контейнер в который будут записываться имена тегов, они будут ключами для других контейнеров, в них будут храниться наши хеши айдишников кешируемых методов. Для кеш фронтенда был взят 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.
Теги:
Хабы:
Всего голосов 16: ↑8 и ↓80
Комментарии27

Публикации

Истории

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань