|
TYPO3 API
SVNRelease
|
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 ?>
1.8.0