📜 ⬆️ ⬇️

Tagging cache in Yii

In Yii, dependencies ( Dependency ) are provided to check the relevance of the cache. They certainly allow a lot, but, as always, not enough. I wanted to be able to tag the cache with tags so that when you remove any tag, the entire cache marked with this tag becomes irrelevant.
Googling found a couple of articles about this:

The first article is cached with a single tag dependency.
In the second article, there is already a dependence on several tags, but the use of the described construction introduces some unexpected effect in the project. Hoping that any tag will live longer than the record marked with this tag seems to me slightly frivolous.
Not finding a suitable solution, I wrote my own one based on Dklab_Cache_Backend_TagEmuWrapper, but in Yii style.

I will formulate the task that needed to be implemented.
  1. Any value can (but not necessarily) be tagged with one or more tags.
  2. Need the ability to remove the tag
  3. When deleting a tag, the entire cache marked with this tag becomes irrelevant.

Now implementation.
  1. In order to be able to check the relevance of the tags, we will store along with the tag its version.
  2. Together with the tagged entry, the list of tags with which it is marked is saved in the cache.
  3. When checking the relevance of a cache entry, we pull out all the tags that mark the cache entry, and compare them with the tags stored in the cache.

I will describe on fingers what the last three conditions mean.
Suppose we save a cache entry with the key "key" and the value "value". Mark this entry with tags «tagA», «tagB».
Something like this:
$dependency = new \Cache\Tagged\Dependency(array('tagA', 'tagB')); Yii::app()->cache->set('key', 'value', 0, $dependency); 

at the same time, three entries are stored in the cache:

In fact, Yii adds a copy of the Dependency object to the array, in order to later verify the relevance of the cache. (But this is not reflected here so as not to clutter the text.)

Now suppose we are reading a cache entry.
The following steps are performed:
  1. Record is read with key 'key'
  2. Read tags glued to the record
  3. Tags read from the cache are compared with copies of tags in the record.
  4. If the tags do not match, then the conclusion is that the cache is outdated.

That's how it turns out everything is simple. And here is the code:
 /** * protected/components/cache/Tagged/Dependency.php */ namespace Cache\Tagged; class Dependency implements \ICacheDependency { //  ,    public $_tags = null; //      \ICache public $_backend; //     public $_tag_versions = null; /** *     ,    */ function __construct(array $tags) { $this->_tags = $tags; } function initBackend() { $this->_backend = \Yii::app()->cache; } /** *        . *              property:_tags */ public function evaluateDependency() { $this->initBackend(); $this->_tag_versions = null; if($this->_tags === null || !is_array($this->_tags)) { return; } if (!$this->_backend) return; $tagsWithVersion = array(); foreach ($this->_tags as $tag) { $mangledTag = Helper::mangleTag($tag); $tagVersion = $this->_backend->get($mangledTag); if ($tagVersion === false) { $tagVersion = Helper::generateNewTagVersion(); $this->_backend->set($mangledTag, $tagVersion, 0); } $tagsWithVersion[$tag] = $tagVersion; } $this->_tag_versions = $tagsWithVersion; return; } /** *  true,     */ public function getHasChanged() { $this->initBackend(); if ($this->_tag_versions === null || !is_array($this->_tag_versions)) { return true; } //          $allMangledTagValues = $this->_backend->mget(Helper::mangleTags(array_keys($this->_tag_versions))); //     dependency. ..  foreach ($this->_tag_versions as $tag => $savedTagVersion) { $mangleTag = Helper::mangleTag($tag); //   "",     if (!isset($allMangledTagValues[$mangleTag])) { return true; } $actualTagVersion = $allMangledTagValues[$mangleTag]; //    ,    if ($actualTagVersion !== $savedTagVersion) { return true; } } return false; } } 


and helper to this addiction
')
 namespace Cache\Tagged; /** * protected/components/cache/Tagged/Helper.php */ class Helper { const VERSION = "0.01"; static private $_cache = null; static public function init(\ICache $cacheId = null) { if ($cacheId === null) { if (self::$_cache !== null) { return true; } //       self::$_cache = \Yii::app()->cache; } else { self::$_cache = $cacheId; } return (self::$_cache !== null); } /** *    *       ,    */ static public function deleteByTags($tags = array()) { if (!self::init()) return false; if (is_string($tags)) { $tags = array($tags); } if (is_array($tags)) { foreach ($tags as $tag) { self::$_cache->delete(self::mangleTag($tag)); } } return true; } /** *       */ static public function mangleTag($tag) { return get_called_class() . "_" . self::VERSION . "_" . $tag; } /** *   mangleTag        * @see self::_mangleTag */ static public function mangleTags($tags) { foreach ($tags as $i => $tag) { $tags[$i] = self::mangleTag($tag); } return $tags; } /** *        */ static public function generateNewTagVersion() { static $counter = 0; $counter++; return md5(microtime() . getmypid() . uniqid('')) . '_' . $counter; } } 


because I used the namespace in the code, then it will be necessary to prescribe an alias in the config
 Yii::setPathOfAlias('Cache', $basepath . DIRECTORY_SEPARATOR . 'components/cache'); 

and you can use a new dependency, like this:
 //        $cache = \Yii::app()->cache; //     $dependency = new \Cache\Tagged\Dependency(array('c', 'd', 'e')); //         $cache->set('LetterA', 'A', 0, $dependency); // ,      var_dump($cache->get('LetterA')); //   (      ) \Cache\Tagged\Helper::deleteByTags(array('d')); // ,       var_dump($cache->get('LetterA')); 

For complete happiness, let's make transparent caching of CActiveRecord data models. (You need to apply a new class somewhere)
Create a new protected / components / ActiveRecord.php file with the following contents:
 class ActiveRecord extends CActiveRecord { //    const CACHE_DURATION = 0; protected function beforeFind() { $tags = array($this->tableName()); $this->cache(self::CACHE_DURATION, new \Cache\Tagged\Dependency(array($tags))); parent::beforeFind(); } protected function afterSave() { \Cache\Tagged\Helper::deleteByTags($this->tableName()); parent::afterSave(); } protected function afterDelete() { \Cache\Tagged\Helper::deleteByTags($this->tableName()); parent::afterDelete(); } } 

We inherit it instead of CActiveRecord and watch for a decrease in connections to the database.

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


All Articles