📜 ⬆️ ⬇️

Teach Zend Memcache to work with tags.

In the project, where I am a developer, the cache is used. Just want to make a reservation, the project is highly loaded, about two thousand people per day. A convenient solution to unload the database was the use of memkesh. Since the project on the Zend Framework, the implementation of the cache, respectively, took it. But as it turned out, not the most successful implementation, since the work with tags is completely absent, it does not allow us to clean the cache selectively.

How can I implement selective cache cleaning? It's very simple, we create a container in which the tag names will be recorded, they will be keys for other containers, our hashes of aydishniki of cached methods will be stored in them. Zend_Cache_Frontend_Class was taken for the cache frontend. It is this one that generates the hashes of the data keys of the cached methods.

It looks like this:

')
Tags_Container – , .
MemcacheTag1, MemcacheTag2 MemcacheTagN – , Tags_Container , .


Especially without going into the description of the implementation offer a turnkey solution.
Create your backend file 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); } } 

How to use?

I cite the example code, I warn you about the settings of the backend and frontend, you can transfer in another way, the given code is only to understand how to use.

 $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')); 

Instead of the standard Memcached backend, you must pass your backend class “My_Cache_Backend_Memcached”, you also need to specify $ customBackendNaming = true, this is the 6th parameter in the call to the Zend_Cache :: factory.

Default_Models_MemcacheData1 and Default_Models_MemcacheData2 are our cached classes, they are completely identical. I give an example of one of them:
 class Default_Models_MemcacheData1 { public function cachedMethod() { return rand(111, 999); } } 

As you can see from the code, each time the cachedMethod method is called, we must receive a random value.
 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:'); } 

When executing the code, we get something like the following:
cached data: int(468)
cached data: int(676)
ached data: int(721)
ached data: int(182)
cached data: int(414)
cached data: int(561)


You can check the cache in action by running the following code
 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:'); } 

The situation will change, we will get approximately the following 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)


Subsequently, the situation does not change.

To clear the cache, use the following code.
 $cachedClass1->clean( Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('Memcached_Tag1') ); 

Make sure that the cache is cleaned according to the transmitted tag.

The problem of selective cache cleaning was solved, there was an increase in performance.

* Thanks homm for the comments. Now the lifetime for tags is calculated based on the settings, or if the $ specificLifetime parameter passed is longer, then it is taken. Also added check for changes to the common container, if the tags have changed.

ps I want to warn php developers, you can use this code at your own peril and risk. This code may be a panacea for particular cases. And is not explosive.
Using this code, remember that tag names are also keys, as are the transmitted id.

Source: https://habr.com/ru/post/137150/


All Articles