TYPO3 API  SVNRelease
class.t3lib_cache_backend_redisbackend.php
Go to the documentation of this file.
00001 <?php
00002 /***************************************************************
00003  *  Copyright notice
00004  *
00005  *  (c) 2010-2011 Christian Kuhn <lolli@schwarzbu.ch>
00006  *  All rights reserved
00007  *
00008  *  This script is part of the TYPO3 project. The TYPO3 project is
00009  *  free software; you can redistribute it and/or modify
00010  *  it under the terms of the GNU General Public License as published by
00011  *  the Free Software Foundation; either version 2 of the License, or
00012  *  (at your option) any later version.
00013  *
00014  *  The GNU General Public License can be found at
00015  *  http://www.gnu.org/copyleft/gpl.html.
00016  *
00017  *  This script is distributed in the hope that it will be useful,
00018  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00019  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00020  *  GNU General Public License for more details.
00021  *
00022  *  This copyright notice MUST APPEAR in all copies of the script!
00023  ***************************************************************/
00024 
00025 /**
00026  * A caching backend which stores cache entries by using Redis with phpredis
00027  * PHP module. Redis is a noSQL database with very good scaling characteristics
00028  * in proportion to the amount of entries and data size.
00029  *
00030  * @see http://code.google.com/p/redis/
00031  * @see http://github.com/owlient/phpredis
00032  *
00033  * Warning:
00034  * Redis and phpredis are young projects with very high development speed.
00035  * This implementation should be considered as experimental for now,
00036  * internals might break or change while the dependent projects mature.
00037  *
00038  * Successfully tested with:
00039  * - redis
00040  *   version 2.0.0-rc2, version 1.2.0 does not work
00041  *   git version 9fd01051bf8400babcca73a76a67dfc1847633ff from 2010-11-12
00042  * - phpredis
00043  *   git version 0abb9e5ec07b8a8c20b5 from 2010-07-18
00044  *   git version 12769b03c8ec17b25573e0453003712011bba241 from 2010-11-08
00045  *
00046  * Implementation based on ext:rediscache by Christopher Hlubek - networkteam GmbH
00047  *
00048  * This backend uses the following types of redis keys:
00049  * - identData:xxx, value type "string", volatile, expires after given lifetime
00050  *   xxx is the given identifier name, value is the cache data
00051  * - identTags:xxx, value type "set"
00052  *   xxx is the given identifier name, value is a set of associated tags.
00053  *   This is a "reverse" tag index. It provides quick access for all tags
00054  *   associated with this identifier and is used when removing the identifier.
00055  * - tagIdents:xxx, value type "set"
00056  *   xxx is a tag name, value is a set of associated identifiers.
00057  *   This is "forward" tag index. It is mainly used for flushing content by tag.
00058  * - temp:xxx, value type "set"
00059  *   xxx is a unique id, value is a set of identifiers. Used as temporary key
00060  *   used in flushByTag() and flushByTags(), removed after usage again.
00061  *
00062  * Each cache using this backend should use an own redis database to
00063  * avoid namespace problems. By default redis has 16 databases which are
00064  * identified with numbers 0 .. 15. setDatabase() can be used to select one.
00065  * The unit tests use and flush database numbers 0 and 1, production use should start from 2.
00066  *
00067  * @package TYPO3
00068  * @subpackage t3lib_cache
00069  * @api
00070  * @scope prototype
00071  */
00072 class t3lib_cache_backend_RedisBackend extends t3lib_cache_backend_AbstractBackend {
00073 
00074     /**
00075      * Faked unlimited lifetime = 31536000 (1 Year).
00076      * In redis an entry does not have a lifetime by default (it's not "volatile").
00077      * Entries can be made volatile either with EXPIRE after it has been SET,
00078      * or with SETEX, which is a combined SET and EXPIRE command.
00079      * But an entry can not be made "unvolatile" again. To set a volatile entry to
00080      * not volatile again, it must be DELeted and SET without a following EXPIRE.
00081      * To save these additional calls on every set(),
00082      * we just make every entry volatile and treat a high number as "unlimited"
00083      *
00084      * @see http://code.google.com/p/redis/wiki/ExpireCommand
00085      * @var integer Faked unlimited lifetime
00086      */
00087     const FAKED_UNLIMITED_LIFETIME = 31536000;
00088 
00089     /**
00090      * @var string Key prefix for identifier->data entries
00091      */
00092     const IDENTIFIER_DATA_PREFIX = 'identData:';
00093 
00094     /**
00095      * @var string Key prefix for identifier->tags sets
00096      */
00097     const IDENTIFIER_TAGS_PREFIX = 'identTags:';
00098 
00099     /**
00100      * @var string Key prefix for tag->identifiers sets
00101      */
00102     const TAG_IDENTIFIERS_PREFIX = 'tagIdents:';
00103 
00104     /**
00105      * @var Redis Instance of the PHP redis class
00106      */
00107     protected $redis;
00108 
00109     /**
00110      * @var boolean Indicates wether the server is connected
00111      */
00112     protected $connected = FALSE;
00113 
00114     /**
00115      * @var string Hostname / IP of the Redis server, defaults to 127.0.0.1.
00116      */
00117     protected $hostname = '127.0.0.1';
00118 
00119     /**
00120      * @var integer Port of the Redis server, defaults to 6379
00121      */
00122     protected $port = 6379;
00123 
00124     /**
00125      * @var integer Number of selected database, defaults to 0
00126      */
00127     protected $database = 0;
00128 
00129     /**
00130      * @var string Password for redis authentication
00131      */
00132     protected $password = '';
00133 
00134     /**
00135      * @var boolean Indicates wether data is compressed or not (requires php zlib)
00136      */
00137     protected $compression = FALSE;
00138 
00139     /**
00140      * @var integer -1 to 9, indicates zlib compression level: -1 = default level 6, 0 = no compression, 9 maximum compression
00141      */
00142     protected $compressionLevel = -1;
00143 
00144     /**
00145      * Construct this backend
00146      *
00147      * @param array $options Configuration options
00148      * @throws t3lib_cache_Exception if php redis module is not loaded
00149      * @author Christopher Hlubek <hlubek@networkteam.com>
00150      * @author Christian Kuhn <lolli@schwarzbu.ch>
00151      */
00152     public function __construct(array $options = array()) {
00153         if (!extension_loaded('redis')) {
00154             throw new t3lib_cache_Exception(
00155                 'The PHP extension "redis" must be installed and loaded in order to use the redis backend.',
00156                 1279462933
00157             );
00158         }
00159 
00160         parent::__construct($options);
00161 
00162         $this->initializeObject();
00163     }
00164 
00165     /**
00166      * Initializes the redis backend
00167      *
00168      * @return void
00169      * @throws t3lib_cache_Exception if access to redis with password is denied or if database selection fails
00170      * @author Christian Kuhn <lolli@schwarzbu.ch>
00171      */
00172     protected function initializeObject() {
00173         $this->redis = new Redis();
00174 
00175         try {
00176             $this->connected = $this->redis->connect($this->hostname, $this->port);
00177         } catch (Exception $e) {
00178             t3lib_div::sysLog('Unable to connect to redis server.', 'core', 3);
00179         }
00180 
00181         if ($this->connected) {
00182             if (strlen($this->password)) {
00183                 $success = $this->redis->auth($this->password);
00184                 if (!$success) {
00185                     throw new t3lib_cache_Exception(
00186                         'The given password was not accepted by the redis server.',
00187                         1279765134
00188                     );
00189                 }
00190             }
00191 
00192             if ($this->database > 0) {
00193                 $success = $this->redis->select($this->database);
00194                 if (!$success) {
00195                     throw new t3lib_cache_Exception(
00196                         'The given database "' . $this->database . '" could not be selected.',
00197                         1279765144
00198                     );
00199                 }
00200             }
00201         }
00202     }
00203 
00204     /**
00205      * Setter for server hostname
00206      *
00207      * @param string $hostname Hostname
00208      * @return void
00209      * @author Christopher Hlubek <hlubek@networkteam.com>
00210      * @author Christian Kuhn <lolli@schwarzbu.ch>
00211      * @api
00212      */
00213     public function setHostname($hostname) {
00214         $this->hostname = $hostname;
00215     }
00216 
00217     /**
00218      * Setter for server port
00219      *
00220      * @param integer $port Port
00221      * @return void
00222      * @author Christopher Hlubek <hlubek@networkteam.com>
00223      * @author Christian Kuhn <lolli@schwarzbu.ch>
00224      * @api
00225      */
00226     public function setPort($port) {
00227         $this->port = $port;
00228     }
00229 
00230     /**
00231      * Setter for database number
00232      *
00233      * @param integer $database Database
00234      * @return void
00235      * @throws InvalidArgumentException if database number is not valid
00236      * @author Christian Kuhn <lolli@schwarzbu.ch>
00237      * @api
00238      */
00239     public function setDatabase($database) {
00240         if (!is_integer($database)) {
00241             throw new InvalidArgumentException(
00242                 'The specified database number is of type "' . gettype($database) . '" but an integer is expected.',
00243                 1279763057
00244             );
00245         }
00246         if ($database < 0) {
00247             throw new InvalidArgumentException(
00248                 'The specified database "' . $database . '" must be greater or equal than zero.',
00249                 1279763534
00250             );
00251         }
00252 
00253         $this->database = $database;
00254     }
00255 
00256     /**
00257      * Setter for authentication password
00258      *
00259      * @param string $password Password
00260      * @return void
00261      * @author Christian Kuhn <lolli@schwarzbu.ch>
00262      * @api
00263      */
00264     public function setPassword($password) {
00265         $this->password = $password;
00266     }
00267 
00268     /**
00269      * Enable data compression
00270      *
00271      * @param boolean $compression TRUE to enable compression
00272      * @return void
00273      * @throws InvalidArgumentException if compression parameter is not of type boolean
00274      * @author Christian Kuhn <lolli@schwarzbu.ch>
00275      * @api
00276      */
00277     public function setCompression($compression) {
00278         if (!is_bool($compression)) {
00279             throw new InvalidArgumentException(
00280                 'The specified compression of type "' . gettype($compression) . '" but a boolean is expected.',
00281                 1289679153
00282             );
00283         }
00284 
00285         $this->compression = $compression;
00286     }
00287 
00288     /**
00289      * Set data compression level.
00290      * If compression is enabled and this is not set,
00291      * gzcompress default level will be used.
00292      *
00293      * @param integer $compressionLevel -1 to 9: Compression level
00294      * @return void
00295      * @throws InvalidArgumentException if compressionLevel parameter is not within allowed bounds
00296      * @author Christian Kuhn <lolli@schwarzbu.ch>
00297      * @api
00298      */
00299     public function setCompressionLevel($compressionLevel) {
00300         if (!is_integer($compressionLevel)) {
00301             throw new InvalidArgumentException(
00302                 'The specified compression of type "' . gettype($compressionLevel) . '" but an integer is expected.',
00303                 1289679154
00304             );
00305         }
00306 
00307         if ($compressionLevel >= -1 && $compressionLevel <= 9) {
00308             $this->compressionLevel = $compressionLevel;
00309         } else {
00310             throw new InvalidArgumentException(
00311                 'The specified compression level must be an integer between -1 and 9.',
00312                 1289679155
00313             );
00314         }
00315     }
00316 
00317     /**
00318      * Save data in the cache
00319      *
00320      * Scales O(1) with number of cache entries
00321      * Scales O(n) with number of tags
00322      *
00323      * @param string $entryIdentifier Identifier for this specific cache entry
00324      * @param string $data Data to be stored
00325      * @param array $tags Tags to associate with this cache entry
00326      * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, default lifetime is used. "0" means unlimited lifetime.
00327      * @return void
00328      * @throws InvalidArgumentException if identifier is not valid
00329      * @throws t3lib_cache_Exception_InvalidData if data is not a string
00330      * @author Christopher Hlubek <hlubek@networkteam.com>
00331      * @author Christian Kuhn <lolli@schwarzbu.ch>
00332      * @api
00333      */
00334     public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
00335         if (!is_string($entryIdentifier)) {
00336             throw new InvalidArgumentException(
00337                 'The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.',
00338                 1279470252
00339             );
00340         }
00341         if (!is_string($data)) {
00342             throw new t3lib_cache_Exception_InvalidData(
00343                 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
00344                 1279469941
00345             );
00346         }
00347 
00348         $lifetimeIsNull = is_null($lifetime);
00349         $lifetimeIsInteger = is_integer($lifetime);
00350 
00351         if (!$lifetimeIsNull && !$lifetimeIsInteger) {
00352             throw new InvalidArgumentException(
00353                 'The specified lifetime is of type "' . gettype($lifetime) . '" but a string or NULL is expected.',
00354                 1279488008
00355             );
00356         }
00357         if ($lifetimeIsInteger && $lifetime < 0) {
00358             throw new InvalidArgumentException(
00359                 'The specified lifetime "' . $lifetime . '" must be greater or equal than zero.',
00360                 1279487573
00361             );
00362         }
00363 
00364         if ($this->connected) {
00365             $expiration = $lifetimeIsNull ? $this->defaultLifetime : $lifetime;
00366             $expiration = $expiration === 0 ? self::FAKED_UNLIMITED_LIFETIME : $expiration;
00367 
00368             if ($this->compression) {
00369                 $data = gzcompress($data, $this->compressionLevel);
00370             }
00371 
00372             $this->redis->setex(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, $expiration, $data);
00373 
00374             $addTags = $tags;
00375             $removeTags = array();
00376             $existingTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
00377             if (!empty($existingTags)) {
00378                 $addTags = array_diff($tags, $existingTags);
00379                 $removeTags = array_diff($existingTags, $tags);
00380             }
00381 
00382             if (count($removeTags) > 0 || count($addTags) > 0) {
00383                 $queue = $this->redis->multi(Redis::PIPELINE);
00384                 foreach ($removeTags as $tag) {
00385                     $queue->sRemove(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
00386                     $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
00387                 }
00388 
00389                 foreach ($addTags as $tag) {
00390                     $queue->sAdd(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
00391                     $queue->sAdd(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
00392                 }
00393                 $queue->exec();
00394             }
00395         }
00396     }
00397 
00398     /**
00399      * Loads data from the cache.
00400      *
00401      * Scales O(1) with number of cache entries
00402      *
00403      * @param string $entryIdentifier An identifier which describes the cache entry to load
00404      * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
00405      * @throws InvalidArgumentException if identifier is not a string
00406      * @author Christopher Hlubek <hlubek@networkteam.com>
00407      * @author Christian Kuhn <lolli@schwarzbu.ch>
00408      * @api
00409      */
00410     public function get($entryIdentifier) {
00411         if (!is_string($entryIdentifier)) {
00412             throw new InvalidArgumentException(
00413                 'The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.',
00414                 1279470253
00415             );
00416         }
00417 
00418         $storedEntry = FALSE;
00419         if ($this->connected) {
00420             $storedEntry = $this->redis->get(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
00421         }
00422 
00423         if ($this->compression && strlen($storedEntry) > 0) {
00424             $storedEntry = gzuncompress($storedEntry);
00425         }
00426 
00427         return $storedEntry;
00428     }
00429 
00430     /**
00431      * Checks if a cache entry with the specified identifier exists.
00432      *
00433      * Scales O(1) with number of cache entries
00434      *
00435      * @param string $entryIdentifier Identifier specifying the cache entry
00436      * @return boolean TRUE if such an entry exists, FALSE if not
00437      * @throws InvalidArgumentException if identifier is not a string
00438      * @author Christopher Hlubek <hlubek@networkteam.com>
00439      * @author Christian Kuhn <lolli@schwarzbu.ch>
00440      * @api
00441      */
00442     public function has($entryIdentifier) {
00443         if (!is_string($entryIdentifier)) {
00444             throw new InvalidArgumentException(
00445                 'The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.',
00446                 1279470254
00447             );
00448         }
00449         return $this->connected && $this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
00450     }
00451 
00452     /**
00453      * Removes all cache entries matching the specified identifier.
00454      *
00455      * Scales O(1) with number of cache entries
00456      * Scales O(n) with number of tags
00457      *
00458      * @param string $entryIdentifier Specifies the cache entry to remove
00459      * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
00460      * @throws InvalidArgumentException if identifier is not a string
00461      * @author Christopher Hlubek <hlubek@networkteam.com>
00462      * @author Christian Kuhn <lolli@schwarzbu.ch>
00463      * @api
00464      */
00465     public function remove($entryIdentifier) {
00466         if (!is_string($entryIdentifier)) {
00467             throw new InvalidArgumentException(
00468                 'The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.',
00469                 1279470255
00470             );
00471         }
00472 
00473         $elementsDeleted = FALSE;
00474         if ($this->connected) {
00475             if ($this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier)) {
00476                 $assignedTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
00477 
00478                 $queue = $this->redis->multi(Redis::PIPELINE);
00479                 foreach ($assignedTags as $tag) {
00480                     $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
00481                 }
00482                 $queue->delete(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
00483                 $queue->exec();
00484                 $elementsDeleted = TRUE;
00485             }
00486         }
00487 
00488         return $elementsDeleted;
00489     }
00490 
00491     /**
00492      * Finds and returns all cache entry identifiers which are tagged by the
00493      * specified tag.
00494      *
00495      * Scales O(1) with number of cache entries
00496      * Scales O(n) with number of tag entries
00497      *
00498      * @param string $tag The tag to search for
00499      * @return array An array of entries with all matching entries. An empty array if no entries matched
00500      * @throws InvalidArgumentException if tag is not a string
00501      * @author Christopher Hlubek <hlubek@networkteam.com>
00502      * @author Christian Kuhn <lolli@schwarzbu.ch>
00503      * @api
00504      */
00505     public function findIdentifiersByTag($tag) {
00506         if (!is_string($tag)) {
00507             throw new InvalidArgumentException(
00508                 'The specified tag is of type "' . gettype($tag) . '" but a string is expected.',
00509                 1279569759
00510             );
00511         }
00512 
00513         $foundIdentifiers = array();
00514         if ($this->connected) {
00515             $foundIdentifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
00516         }
00517 
00518         return $foundIdentifiers;
00519     }
00520 
00521     /**
00522      * Finds and returns all cache entry identifiers which are tagged
00523      * with all of the specified tags.
00524      *
00525      * Scales O(n) with number of tags
00526      *
00527      * @param array $tags Array of tags to search for
00528      * @return array An array with identifiers of all matching entries. An empty array if no entries matched
00529      * @author Christopher Hlubek <hlubek@networkteam.com>
00530      * @author Christian Kuhn <lolli@schwarzbu.ch>
00531      * @api
00532      */
00533     public function findIdentifiersByTags(array $tags) {
00534         $foundIdentifiers = array();
00535 
00536         if ($this->connected) {
00537             $tagsWithPrefix = array();
00538             foreach ($tags as $tag) {
00539                 $tagsWithPrefix[] = self::TAG_IDENTIFIERS_PREFIX . $tag;
00540             }
00541             $foundIdentifiers = $this->redis->sInter($tagsWithPrefix);
00542         }
00543 
00544         return $foundIdentifiers;
00545     }
00546 
00547     /**
00548      * Removes all cache entries of this cache.
00549      *
00550      * Scales O(1) with number of cache entries
00551      *
00552      * @return void
00553      * @author Christopher Hlubek <hlubek@networkteam.com>
00554      * @author Christian Kuhn <lolli@schwarzbu.ch>
00555      * @api
00556      */
00557     public function flush() {
00558         if ($this->connected) {
00559             $this->redis->flushdb();
00560         }
00561     }
00562 
00563     /**
00564      * Removes all cache entries of this cache which are tagged with the specified tag.
00565      *
00566      * Scales O(1) with number of cache entries
00567      * Scales O(n^2) with number of tag entries
00568      *
00569      * @param string $tags Tag the entries must have
00570      * @return void
00571      * @throws InvalidArgumentException if identifier is not a string
00572      * @author Christopher Hlubek <hlubek@networkteam.com>
00573      * @author Christian Kuhn <lolli@schwarzbu.ch>
00574      * @api
00575      */
00576     public function flushByTag($tag) {
00577         if (!is_string($tag)) {
00578             throw new InvalidArgumentException(
00579                 'The specified tag is of type "' . gettype($tag) . '" but a string is expected.',
00580                 1279578078
00581             );
00582         }
00583 
00584         if ($this->connected) {
00585             $identifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
00586 
00587             if (count($identifiers) > 0) {
00588                 $this->removeIdentifierEntriesAndRelations($identifiers, array($tag));
00589             }
00590         }
00591     }
00592 
00593     /**
00594      * Removes all cache entries of this cache which are tagged with one of the specified tags.
00595      *
00596      * Scales O(1) with number of cache entries
00597      * Scales O(n^2) with number of tags
00598      *
00599      * @param array $tags Tags the entries must have
00600      * @return void
00601      * @author Christian Kuhn <lolli@schwarzbu.ch>
00602      * @api
00603      */
00604     public function flushByTags(array $tags) {
00605         if ($this->connected) {
00606             $prefixedKeysToDelete = array();
00607             foreach ($tags as $tag) {
00608                 $prefixedKeysToDelete[] = self::TAG_IDENTIFIERS_PREFIX . $tag;
00609             }
00610 
00611                 // Get all identifiers tagged with at least one of the given tags
00612             $identifiers = $this->redis->sUnion($prefixedKeysToDelete);
00613 
00614             if (count($identifiers)) {
00615                 $this->removeIdentifierEntriesAndRelations($identifiers, $tags, $prefixedKeysToDelete);
00616             }
00617         }
00618     }
00619 
00620     /**
00621      * With the current internal structure, only the identifier to data entries
00622      * have a redis internal lifetime. If an entry expires, attached
00623      * identifier to tags and tag to identifiers entries will be left over.
00624      * This methods finds those entries and cleans them up.
00625      *
00626      * Scales O(n*m) with number of cache entries (n) and number of tags (m)
00627      *
00628      * @return void
00629      * @author Christian Kuhn <lolli@schwarzbu.ch>
00630      * @author Christopher Hlubek <hlubek@networkteam.com>
00631      * @api
00632      */
00633     public function collectGarbage() {
00634         $identifierToTagsKeys = $this->redis->getKeys(self::IDENTIFIER_TAGS_PREFIX . '*');
00635         foreach ($identifierToTagsKeys as $identifierToTagsKey) {
00636             list(, $identifier) = explode(':', $identifierToTagsKey);
00637                 // Check if the data entry still exists
00638             if (!$this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $identifier)) {
00639                 $tagsToRemoveIdentifierFrom = $this->redis->sMembers($identifierToTagsKey);
00640                 $queue = $this->redis->multi(Redis::PIPELINE);
00641                 $queue->delete($identifierToTagsKey);
00642                 foreach ($tagsToRemoveIdentifierFrom as $tag) {
00643                     $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $identifier);
00644                 }
00645                 $queue->exec();
00646             }
00647         }
00648     }
00649 
00650     /**
00651      * Helper method for flushByTag() and flushByTags()
00652      * Gets list of identifiers and tags and removes all relations of those tags
00653      *
00654      * Scales O(1) with number of cache entries
00655      * Scales O(n^2) with number of tags
00656      *
00657      * @param array $identifiers List of identifiers to remove
00658      * @param array $tags List of tags to be handled
00659      * @return void
00660      * @author Christian Kuhn <lolli@schwarzbu.ch>
00661      * @author Christopher Hlubek <hlubek@networkteam.com>
00662      */
00663     protected function removeIdentifierEntriesAndRelations(array $identifiers, array $tags) {
00664             // Set a temporary entry which holds all identifiers that need to be removed from
00665             // the tag to identifiers sets
00666         $uniqueTempKey = 'temp:' . uniqId();
00667         $prefixedKeysToDelete = array($uniqueTempKey);
00668 
00669         $prefixedIdentifierToTagsKeysToDelete = array();
00670         foreach ($identifiers as $identifier) {
00671             $prefixedKeysToDelete[] = self::IDENTIFIER_DATA_PREFIX . $identifier;
00672             $prefixedIdentifierToTagsKeysToDelete[] = self::IDENTIFIER_TAGS_PREFIX . $identifier;
00673         }
00674         foreach ($tags as $tag) {
00675             $prefixedKeysToDelete[] = self::TAG_IDENTIFIERS_PREFIX . $tag;
00676         }
00677 
00678         $tagToIdentifiersSetsToRemoveIdentifiersFrom = $this->redis->sUnion($prefixedIdentifierToTagsKeysToDelete);
00679 
00680             // Remove the tag to identifier set of the given tags, they will be removed anyway
00681         $tagToIdentifiersSetsToRemoveIdentifiersFrom = array_diff($tagToIdentifiersSetsToRemoveIdentifiersFrom, $tags);
00682 
00683             // Diff all identifiers that must be removed from tag to identifiers sets off from a
00684             // tag to identifiers set and store result in same tag to identifiers set again
00685         $queue = $this->redis->multi(Redis::PIPELINE);
00686         foreach ($identifiers as $identifier) {
00687             $queue->sAdd($uniqueTempKey, $identifier);
00688         }
00689         foreach ($tagToIdentifiersSetsToRemoveIdentifiersFrom as $tagToIdentifiersSet) {
00690             $queue->sDiffStore(
00691                 self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet,
00692                 self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet,
00693                 $uniqueTempKey
00694             );
00695         }
00696 
00697         $queue->delete(array_merge($prefixedKeysToDelete, $prefixedIdentifierToTagsKeysToDelete));
00698         $queue->exec();
00699     }
00700 }
00701 
00702 ?>