Redis.php 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279
  1. <?php
  2. /*
  3. ==New BSD License==
  4. Copyright (c) 2013, Colin Mollenhour
  5. All rights reserved.
  6. Redistribution and use in source and binary forms, with or without
  7. modification, are permitted provided that the following conditions are met:
  8. * Redistributions of source code must retain the above copyright
  9. notice, this list of conditions and the following disclaimer.
  10. * Redistributions in binary form must reproduce the above copyright
  11. notice, this list of conditions and the following disclaimer in the
  12. documentation and/or other materials provided with the distribution.
  13. * The name of Colin Mollenhour may not be used to endorse or promote products
  14. derived from this software without specific prior written permission.
  15. * The class name must remain as Cm_Cache_Backend_Redis.
  16. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  17. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
  20. DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. /**
  28. * Redis adapter for Zend_Cache
  29. *
  30. * @copyright Copyright (c) 2013 Colin Mollenhour (http://colin.mollenhour.com)
  31. * @license http://framework.zend.com/license/new-bsd New BSD License
  32. * @author Colin Mollenhour (http://colin.mollenhour.com)
  33. */
  34. class Cm_Cache_Backend_Redis extends Zend_Cache_Backend implements Zend_Cache_Backend_ExtendedInterface
  35. {
  36. const SET_IDS = 'zc:ids';
  37. const SET_TAGS = 'zc:tags';
  38. const PREFIX_KEY = 'zc:k:';
  39. const PREFIX_TAG_IDS = 'zc:ti:';
  40. const FIELD_DATA = 'd';
  41. const FIELD_MTIME = 'm';
  42. const FIELD_TAGS = 't';
  43. const FIELD_INF = 'i';
  44. const MAX_LIFETIME = 2592000; /* Redis backend limit */
  45. const COMPRESS_PREFIX = ":\x1f\x8b";
  46. const DEFAULT_CONNECT_TIMEOUT = 2.5;
  47. const DEFAULT_CONNECT_RETRIES = 1;
  48. const LUA_SAVE_SH1 = '1617c9fb2bda7d790bb1aaa320c1099d81825e64';
  49. const LUA_CLEAN_SH1 = '42ab2fe548aee5ff540123687a2c39a38b54e4a2';
  50. const LUA_GC_SH1 = 'c00416b970f1aa6363b44965d4cf60ee99a6f065';
  51. /** @var Credis_Client */
  52. protected $_redis;
  53. /** @var bool */
  54. protected $_notMatchingTags = FALSE;
  55. /** @var int */
  56. protected $_lifetimelimit = self::MAX_LIFETIME; /* Redis backend limit */
  57. /** @var int|bool */
  58. protected $_compressTags = 1;
  59. /** @var int|bool */
  60. protected $_compressData = 1;
  61. /** @var int */
  62. protected $_compressThreshold = 20480;
  63. /** @var string */
  64. protected $_compressionLib;
  65. /**
  66. * On large data sets SUNION slows down considerably when used with too many arguments
  67. * so this is used to chunk the SUNION into a few commands where the number of set ids
  68. * exceeds this setting.
  69. *
  70. * @var int
  71. */
  72. protected $_sunionChunkSize = 500;
  73. /** @var bool */
  74. protected $_useLua = false;
  75. /** @var integer */
  76. protected $_autoExpireLifetime = 0;
  77. /** @var string */
  78. protected $_autoExpirePattern = '/REQEST/';
  79. /** @var boolean */
  80. protected $_autoExpireRefreshOnLoad = false;
  81. /**
  82. * Lua's unpack() has a limit on the size of the table imposed by
  83. * the number of Lua stack slots that a C function can use.
  84. * This value is defined by LUAI_MAXCSTACK in luaconf.h and for Redis it is set to 8000.
  85. *
  86. * @see https://github.com/antirez/redis/blob/b903145/deps/lua/src/luaconf.h#L439
  87. * @var int
  88. */
  89. protected $_luaMaxCStack = 5000;
  90. /**
  91. * If 'retry_reads_on_master' is truthy then reads will be retried against master when slave returns "(nil)" value
  92. *
  93. * @var boolean
  94. */
  95. protected $_retryReadsOnMaster = false;
  96. /**
  97. * @var stdClass
  98. */
  99. protected $_clientOptions;
  100. /**
  101. * If 'load_from_slaves' is truthy then reads are performed on a randomly selected slave server
  102. *
  103. * @var Credis_Client
  104. */
  105. protected $_slave;
  106. protected function getClientOptions($options = array())
  107. {
  108. $clientOptions = new stdClass();
  109. $clientOptions->forceStandalone = isset($options['force_standalone']) && $options['force_standalone'];
  110. $clientOptions->connectRetries = isset($options['connect_retries']) ? (int) $options['connect_retries'] : self::DEFAULT_CONNECT_RETRIES;
  111. $clientOptions->readTimeout = isset($options['read_timeout']) ? (float) $options['read_timeout'] : NULL;
  112. $clientOptions->password = isset($options['password']) ? $options['password'] : NULL;
  113. $clientOptions->database = isset($options['database']) ? (int) $options['database'] : 0;
  114. $clientOptions->persistent = isset($options['persistent']) ? $options['persistent'] : '';
  115. $clientOptions->timeout = isset($options['timeout']) ? $options['timeout'] : self::DEFAULT_CONNECT_TIMEOUT;
  116. return $clientOptions;
  117. }
  118. /**
  119. * Construct Zend_Cache Redis backend
  120. * @param array $options
  121. * @return \Cm_Cache_Backend_Redis
  122. */
  123. public function __construct($options = array())
  124. {
  125. if ( empty($options['server']) ) {
  126. Zend_Cache::throwException('Redis \'server\' not specified.');
  127. }
  128. $port = isset($options['port']) ? $options['port'] : 6379;
  129. $slaveSelect = isset($options['slave_select_callable']) && is_callable($options['slave_select_callable']) ? $options['slave_select_callable'] : null;
  130. $sentinelMaster = empty($options['sentinel_master']) ? NULL : $options['sentinel_master'];
  131. $this->_clientOptions = $this->getClientOptions($options);
  132. // If 'sentinel_master' is specified then server is actually sentinel and master address should be fetched from server.
  133. if ($sentinelMaster) {
  134. $sentinelClientOptions = isset($options['sentinel']) && is_array($options['sentinel'])
  135. ? $this->getClientOptions($options['sentinel'] + $options)
  136. : $this->_clientOptions;
  137. $servers = preg_split('/\s*,\s*/', trim($options['server']), NULL, PREG_SPLIT_NO_EMPTY);
  138. $sentinel = NULL;
  139. $exception = NULL;
  140. for ($i = 0; $i <= $sentinelClientOptions->connectRetries; $i++) // Try each sentinel in round-robin fashion
  141. foreach ($servers as $server) {
  142. try {
  143. $sentinelClient = new Credis_Client($server, NULL, $sentinelClientOptions->timeout, $sentinelClientOptions->persistent);
  144. $sentinelClient->forceStandalone();
  145. $sentinelClient->setMaxConnectRetries(0);
  146. if ($sentinelClientOptions->readTimeout) {
  147. $sentinelClient->setReadTimeout($sentinelClientOptions->readTimeout);
  148. }
  149. // Sentinel currently doesn't support AUTH
  150. //if ($password) {
  151. // $sentinelClient->auth($password) or Zend_Cache::throwException('Unable to authenticate with the redis sentinel.');
  152. //}
  153. $sentinel = new Credis_Sentinel($sentinelClient);
  154. $sentinel
  155. ->setClientTimeout($this->_clientOptions->timeout)
  156. ->setClientPersistent($this->_clientOptions->persistent);
  157. $redisMaster = $sentinel->getMasterClient($sentinelMaster);
  158. $this->_applyClientOptions($redisMaster);
  159. // Verify connected server is actually master as per Sentinel client spec
  160. if ( ! empty($options['sentinel_master_verify'])) {
  161. $roleData = $redisMaster->role();
  162. if ( ! $roleData || $roleData[0] != 'master') {
  163. usleep(100000); // Sleep 100ms and try again
  164. $redisMaster = $sentinel->getMasterClient($sentinelMaster);
  165. $this->_applyClientOptions($redisMaster);
  166. $roleData = $redisMaster->role();
  167. if ( ! $roleData || $roleData[0] != 'master') {
  168. Zend_Cache::throwException('Unable to determine master redis server.');
  169. }
  170. }
  171. }
  172. $this->_redis = $redisMaster;
  173. break 2;
  174. } catch (Exception $e) {
  175. unset($sentinelClient);
  176. $exception = $e;
  177. }
  178. }
  179. if ( ! $this->_redis) {
  180. Zend_Cache::throwException('Unable to connect to a redis sentinel: '.$exception->getMessage(), $exception);
  181. }
  182. // Optionally use read slaves - will only be used for 'load' operation
  183. if ( ! empty($options['load_from_slaves'])) {
  184. $slaves = $sentinel->getSlaveClients($sentinelMaster);
  185. if ($slaves) {
  186. if ($options['load_from_slaves'] == 2) {
  187. array_push($slaves, $this->_redis); // Also send reads to the master
  188. }
  189. if ($slaveSelect) {
  190. $slave = $slaveSelect($slaves, $this->_redis);
  191. } else {
  192. $slaveKey = array_rand($slaves, 1);
  193. $slave = $slaves[$slaveKey]; /* @var $slave Credis_Client */
  194. }
  195. if ($slave instanceof Credis_Client && $slave != $this->_redis) {
  196. try {
  197. $this->_applyClientOptions($slave, TRUE);
  198. $this->_slave = $slave;
  199. } catch (Exception $e) {
  200. // If there is a problem with first slave then skip 'load_from_slaves' option
  201. }
  202. }
  203. }
  204. }
  205. unset($sentinel);
  206. }
  207. // Instantiate Credis_Cluster
  208. else if ( ! empty($options['cluster'])) {
  209. $this->_setupReadWriteCluster($options);
  210. }
  211. // Direct connection to single Redis server
  212. else {
  213. $this->_redis = new Credis_Client($options['server'], $port, $this->_clientOptions->timeout, $this->_clientOptions->persistent);
  214. $this->_applyClientOptions($this->_redis);
  215. // Support loading from a replication slave
  216. if (isset($options['load_from_slave'])) {
  217. if (is_array($options['load_from_slave'])) {
  218. $server = $options['load_from_slave']['server'];
  219. $port = $options['load_from_slave']['port'];
  220. $clientOptions = $this->getClientOptions($options['load_from_slave'] + $options);
  221. } else {
  222. $server = $options['load_from_slave'];
  223. $port = 6379;
  224. $clientOptions = $this->_clientOptions;
  225. }
  226. if (is_string($server)) {
  227. try {
  228. $slave = new Credis_Client($server, $port, $clientOptions->timeout, $clientOptions->persistent);
  229. $this->_applyClientOptions($slave, TRUE, $clientOptions);
  230. $this->_slave = $slave;
  231. } catch (Exception $e) {
  232. // Slave will not be used
  233. }
  234. }
  235. }
  236. }
  237. if ( isset($options['notMatchingTags']) ) {
  238. $this->_notMatchingTags = (bool) $options['notMatchingTags'];
  239. }
  240. if ( isset($options['compress_tags'])) {
  241. $this->_compressTags = (int) $options['compress_tags'];
  242. }
  243. if ( isset($options['compress_data'])) {
  244. $this->_compressData = (int) $options['compress_data'];
  245. }
  246. if ( isset($options['lifetimelimit'])) {
  247. $this->_lifetimelimit = (int) min($options['lifetimelimit'], self::MAX_LIFETIME);
  248. }
  249. if ( isset($options['compress_threshold'])) {
  250. $this->_compressThreshold = (int) $options['compress_threshold'];
  251. if ($this->_compressThreshold < 1) {
  252. $this->_compressThreshold = 1;
  253. }
  254. }
  255. if ( isset($options['automatic_cleaning_factor']) ) {
  256. $this->_options['automatic_cleaning_factor'] = (int) $options['automatic_cleaning_factor'];
  257. } else {
  258. $this->_options['automatic_cleaning_factor'] = 0;
  259. }
  260. if ( isset($options['compression_lib']) ) {
  261. $this->_compressionLib = (string) $options['compression_lib'];
  262. }
  263. else if ( function_exists('snappy_compress') ) {
  264. $this->_compressionLib = 'snappy';
  265. }
  266. else if ( function_exists('lz4_compress')) {
  267. $version = phpversion("lz4");
  268. if (version_compare($version, "0.3.0") < 0)
  269. {
  270. $this->_compressTags = $this->_compressTags > 1 ? true : false;
  271. $this->_compressData = $this->_compressData > 1 ? true : false;
  272. }
  273. $this->_compressionLib = 'l4z';
  274. }
  275. else if ( function_exists('zstd_compress')) {
  276. $version = phpversion("zstd");
  277. if (version_compare($version, "0.4.13") < 0)
  278. {
  279. $this->_compressTags = $this->_compressTags > 1 ? true : false;
  280. $this->_compressData = $this->_compressData > 1 ? true : false;
  281. }
  282. $this->_compressionLib = 'zstd';
  283. }
  284. else if ( function_exists('lzf_compress') ) {
  285. $this->_compressionLib = 'lzf';
  286. }
  287. else {
  288. $this->_compressionLib = 'gzip';
  289. }
  290. $this->_compressPrefix = substr($this->_compressionLib,0,2).self::COMPRESS_PREFIX;
  291. if ( isset($options['sunion_chunk_size']) && $options['sunion_chunk_size'] > 0) {
  292. $this->_sunionChunkSize = (int) $options['sunion_chunk_size'];
  293. }
  294. if (isset($options['use_lua'])) {
  295. $this->_useLua = (bool) $options['use_lua'];
  296. }
  297. if (isset($options['lua_max_c_stack'])) {
  298. $this->_luaMaxCStack = (int) $options['lua_max_c_stack'];
  299. }
  300. if (isset($options['retry_reads_on_master'])) {
  301. $this->_retryReadsOnMaster = (bool) $options['retry_reads_on_master'];
  302. }
  303. if (isset($options['auto_expire_lifetime'])) {
  304. $this->_autoExpireLifetime = (int) $options['auto_expire_lifetime'];
  305. }
  306. if (isset($options['auto_expire_pattern'])) {
  307. $this->_autoExpirePattern = (string) $options['auto_expire_pattern'];
  308. }
  309. if (isset($options['auto_expire_refresh_on_load'])) {
  310. $this->_autoExpireRefreshOnLoad = (bool) $options['auto_expire_refresh_on_load'];
  311. }
  312. }
  313. /**
  314. * Apply common configuration to client instances.
  315. *
  316. * @param Credis_Client $client
  317. */
  318. protected function _applyClientOptions(Credis_Client $client, $forceSelect = FALSE, $clientOptions = null)
  319. {
  320. if ($clientOptions === null) {
  321. $clientOptions = $this->_clientOptions;
  322. }
  323. if ($clientOptions->forceStandalone) {
  324. $client->forceStandalone();
  325. }
  326. $client->setMaxConnectRetries($clientOptions->connectRetries);
  327. if ($clientOptions->readTimeout) {
  328. $client->setReadTimeout($clientOptions->readTimeout);
  329. }
  330. if ($clientOptions->password) {
  331. $client->auth($clientOptions->password) or Zend_Cache::throwException('Unable to authenticate with the redis server.');
  332. }
  333. // Always select database when persistent is used in case connection is re-used by other clients
  334. if ($forceSelect || $clientOptions->database || $client->getPersistence()) {
  335. $client->select($clientOptions->database) or Zend_Cache::throwException('The redis database could not be selected.');
  336. }
  337. }
  338. protected function _setupReadWriteCluster($options) {
  339. $clusterNodes = array();
  340. if (array_key_exists('master', $options['cluster']) && !empty($options['cluster']['master'])) {
  341. foreach ($options['cluster']['master'] as $masterNode) {
  342. if (empty($masterNode['server']) || empty($masterNode['port'])) {
  343. continue;
  344. }
  345. $clusterNodes[] = array(
  346. 'host' => $masterNode['server'],
  347. 'port' => $masterNode['port'],
  348. 'alias' => 'master',
  349. 'master' => true,
  350. 'write_only' => true,
  351. 'timeout' => $this->_clientOptions->timeout,
  352. 'persistent' => $this->_clientOptions->persistent,
  353. 'db' => (int) $options['database'],
  354. );
  355. break; // limit to 1
  356. }
  357. }
  358. if (!empty($clusterNodes) && array_key_exists('slave', $options['cluster']) && !empty($options['cluster']['slave'])) {
  359. foreach ($options['cluster']['slave'] as $slaveNodes) {
  360. if (empty($masterNode['server']) || empty($masterNode['port'])) {
  361. continue;
  362. }
  363. $clusterNodes[] = array(
  364. 'host' => $slaveNodes['server'],
  365. 'port' => $slaveNodes['port'],
  366. 'alias' => 'slave' . count($clusterNodes),
  367. 'timeout' => $this->_clientOptions->timeout,
  368. 'persistent' => $this->_clientOptions->persistent,
  369. 'db' => (int) $options['database'],
  370. 'password' => $options['password'],
  371. );
  372. }
  373. }
  374. if (!empty($clusterNodes)) {
  375. $this->_redis = new Credis_Cluster($clusterNodes);
  376. }
  377. }
  378. /**
  379. * Load value with given id from cache
  380. *
  381. * @param string $id Cache id
  382. * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
  383. * @return bool|string
  384. */
  385. public function load($id, $doNotTestCacheValidity = false)
  386. {
  387. if ($this->_slave) {
  388. $data = $this->_slave->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA);
  389. // Prevent compounded effect of cache flood on asynchronously replicating master/slave setup
  390. if ($this->_retryReadsOnMaster && $data === false) {
  391. $data = $this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA);
  392. }
  393. } else {
  394. $data = $this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_DATA);
  395. }
  396. if ($data === NULL) {
  397. return FALSE;
  398. }
  399. $decoded = $this->_decodeData($data);
  400. if ($this->_autoExpireLifetime === 0 || !$this->_autoExpireRefreshOnLoad) {
  401. return $decoded;
  402. }
  403. $matches = $this->_matchesAutoExpiringPattern($id);
  404. if (!$matches) {
  405. return $decoded;
  406. }
  407. $this->_redis->expire(self::PREFIX_KEY.$id, min($this->_autoExpireLifetime, self::MAX_LIFETIME));
  408. return $decoded;
  409. }
  410. /**
  411. * Test if a cache is available or not (for the given id)
  412. *
  413. * @param string $id Cache id
  414. * @return bool|int False if record is not available or "last modified" timestamp of the available cache record
  415. */
  416. public function test($id)
  417. {
  418. // Don't use slave for this since `test` is usually used for locking
  419. $mtime = $this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_MTIME);
  420. return ($mtime ? $mtime : FALSE);
  421. }
  422. /**
  423. * Save some string datas into a cache record
  424. *
  425. * Note : $data is always "string" (serialization is done by the
  426. * core not by the backend)
  427. *
  428. * @param string $data Datas to cache
  429. * @param string $id Cache id
  430. * @param array $tags Array of strings, the cache record will be tagged by each string entry
  431. * @param bool|int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
  432. * @throws CredisException
  433. * @return boolean True if no problem
  434. */
  435. public function save($data, $id, $tags = array(), $specificLifetime = false)
  436. {
  437. if(!is_array($tags))
  438. $tags = $tags ? array($tags) : array();
  439. else
  440. $tags = array_flip(array_flip($tags));
  441. $lifetime = $this->_getAutoExpiringLifetime($this->getLifetime($specificLifetime), $id);
  442. if ($this->_useLua) {
  443. $sArgs = array(
  444. self::PREFIX_KEY,
  445. self::FIELD_DATA,
  446. self::FIELD_TAGS,
  447. self::FIELD_MTIME,
  448. self::FIELD_INF,
  449. self::SET_TAGS,
  450. self::PREFIX_TAG_IDS,
  451. self::SET_IDS,
  452. $id,
  453. $this->_encodeData($data, $this->_compressData),
  454. $this->_encodeData(implode(',',$tags), $this->_compressTags),
  455. time(),
  456. $lifetime ? 0 : 1,
  457. min($lifetime, self::MAX_LIFETIME),
  458. $this->_notMatchingTags ? 1 : 0
  459. );
  460. $res = $this->_redis->evalSha(self::LUA_SAVE_SH1, $tags, $sArgs);
  461. if (is_null($res)) {
  462. $script =
  463. "local oldTags = redis.call('HGET', ARGV[1]..ARGV[9], ARGV[3]) ".
  464. "redis.call('HMSET', ARGV[1]..ARGV[9], ARGV[2], ARGV[10], ARGV[3], ARGV[11], ARGV[4], ARGV[12], ARGV[5], ARGV[13]) ".
  465. "if (ARGV[13] == '0') then ".
  466. "redis.call('EXPIRE', ARGV[1]..ARGV[9], ARGV[14]) ".
  467. "end ".
  468. "if next(KEYS) ~= nil then ".
  469. "redis.call('SADD', ARGV[6], unpack(KEYS)) ".
  470. "for _, tagname in ipairs(KEYS) do ".
  471. "redis.call('SADD', ARGV[7]..tagname, ARGV[9]) ".
  472. "end ".
  473. "end ".
  474. "if (ARGV[15] == '1') then ".
  475. "redis.call('SADD', ARGV[8], ARGV[9]) ".
  476. "end ".
  477. "if (oldTags ~= false) then ".
  478. "return oldTags ".
  479. "else ".
  480. "return '' ".
  481. "end";
  482. $res = $this->_redis->eval($script, $tags, $sArgs);
  483. }
  484. // Process removed tags if cache entry already existed
  485. if ($res) {
  486. $oldTags = explode(',', $this->_decodeData($res));
  487. if ($remTags = ($oldTags ? array_diff($oldTags, $tags) : FALSE))
  488. {
  489. // Update the id list for each tag
  490. foreach($remTags as $tag)
  491. {
  492. $this->_redis->sRem(self::PREFIX_TAG_IDS . $tag, $id);
  493. }
  494. }
  495. }
  496. return TRUE;
  497. }
  498. // Get list of tags previously assigned
  499. $oldTags = $this->_decodeData($this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_TAGS));
  500. $oldTags = $oldTags ? explode(',', $oldTags) : array();
  501. $this->_redis->pipeline()->multi();
  502. // Set the data
  503. $result = $this->_redis->hMSet(self::PREFIX_KEY.$id, array(
  504. self::FIELD_DATA => $this->_encodeData($data, $this->_compressData),
  505. self::FIELD_TAGS => $this->_encodeData(implode(',',$tags), $this->_compressTags),
  506. self::FIELD_MTIME => time(),
  507. self::FIELD_INF => $lifetime ? 0 : 1,
  508. ));
  509. if( ! $result) {
  510. throw new CredisException("Could not set cache key $id");
  511. }
  512. // Set expiration if specified
  513. if ($lifetime) {
  514. $this->_redis->expire(self::PREFIX_KEY.$id, min($lifetime, self::MAX_LIFETIME));
  515. }
  516. // Process added tags
  517. if ($tags)
  518. {
  519. // Update the list with all the tags
  520. $this->_redis->sAdd( self::SET_TAGS, $tags);
  521. // Update the id list for each tag
  522. foreach($tags as $tag)
  523. {
  524. $this->_redis->sAdd(self::PREFIX_TAG_IDS . $tag, $id);
  525. }
  526. }
  527. // Process removed tags
  528. if ($remTags = ($oldTags ? array_diff($oldTags, $tags) : FALSE))
  529. {
  530. // Update the id list for each tag
  531. foreach($remTags as $tag)
  532. {
  533. $this->_redis->sRem(self::PREFIX_TAG_IDS . $tag, $id);
  534. }
  535. }
  536. // Update the list with all the ids
  537. if($this->_notMatchingTags) {
  538. $this->_redis->sAdd(self::SET_IDS, $id);
  539. }
  540. $this->_redis->exec();
  541. return TRUE;
  542. }
  543. /**
  544. * Remove a cache record
  545. *
  546. * @param string $id Cache id
  547. * @return boolean True if no problem
  548. */
  549. public function remove($id)
  550. {
  551. // Get list of tags for this id
  552. $tags = explode(',', $this->_decodeData($this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_TAGS)));
  553. $this->_redis->pipeline()->multi();
  554. // Remove data
  555. $this->_redis->del(self::PREFIX_KEY.$id);
  556. // Remove id from list of all ids
  557. if($this->_notMatchingTags) {
  558. $this->_redis->sRem( self::SET_IDS, $id );
  559. }
  560. // Update the id list for each tag
  561. foreach($tags as $tag) {
  562. $this->_redis->sRem(self::PREFIX_TAG_IDS . $tag, $id);
  563. }
  564. $result = $this->_redis->exec();
  565. return (bool) $result[0];
  566. }
  567. /**
  568. * @param array $tags
  569. */
  570. protected function _removeByNotMatchingTags($tags)
  571. {
  572. $ids = $this->getIdsNotMatchingTags($tags);
  573. if($ids)
  574. {
  575. $this->_redis->pipeline()->multi();
  576. // Remove data
  577. $this->_redis->del( $this->_preprocessIds($ids));
  578. // Remove ids from list of all ids
  579. if($this->_notMatchingTags) {
  580. $this->_redis->sRem( self::SET_IDS, $ids);
  581. }
  582. $this->_redis->exec();
  583. }
  584. }
  585. /**
  586. * @param array $tags
  587. */
  588. protected function _removeByMatchingTags($tags)
  589. {
  590. $ids = $this->getIdsMatchingTags($tags);
  591. if($ids)
  592. {
  593. $this->_redis->pipeline()->multi();
  594. // Remove data
  595. $this->_redis->del( $this->_preprocessIds($ids));
  596. // Remove ids from list of all ids
  597. if($this->_notMatchingTags) {
  598. $this->_redis->sRem( self::SET_IDS, $ids);
  599. }
  600. $this->_redis->exec();
  601. }
  602. }
  603. /**
  604. * @param array $tags
  605. */
  606. protected function _removeByMatchingAnyTags($tags)
  607. {
  608. if ($this->_useLua) {
  609. $tags = array_chunk($tags, $this->_sunionChunkSize);
  610. foreach ($tags as $chunk) {
  611. $chunk = $this->_preprocessTagIds($chunk);
  612. $args = array(self::PREFIX_KEY, self::SET_TAGS, self::SET_IDS, ($this->_notMatchingTags ? 1 : 0), (int) $this->_luaMaxCStack);
  613. if ( ! $this->_redis->evalSha(self::LUA_CLEAN_SH1, $chunk, $args)) {
  614. $script =
  615. "for i = 1, #KEYS, ARGV[5] do ".
  616. "local keysToDel = redis.call('SUNION', unpack(KEYS, i, math.min(#KEYS, i + ARGV[5] - 1))) ".
  617. "for _, keyname in ipairs(keysToDel) do ".
  618. "redis.call('DEL', ARGV[1]..keyname) ".
  619. "if (ARGV[4] == '1') then ".
  620. "redis.call('SREM', ARGV[3], keyname) ".
  621. "end ".
  622. "end ".
  623. "redis.call('DEL', unpack(KEYS, i, math.min(#KEYS, i + ARGV[5] - 1))) ".
  624. "redis.call('SREM', ARGV[2], unpack(KEYS, i, math.min(#KEYS, i + ARGV[5] - 1))) ".
  625. "end ".
  626. "return true";
  627. $this->_redis->eval($script, $chunk, $args);
  628. }
  629. }
  630. return;
  631. }
  632. $ids = $this->getIdsMatchingAnyTags($tags);
  633. $this->_redis->pipeline()->multi();
  634. if($ids)
  635. {
  636. // Remove data
  637. $this->_redis->del( $this->_preprocessIds($ids));
  638. // Remove ids from list of all ids
  639. if($this->_notMatchingTags) {
  640. $this->_redis->sRem( self::SET_IDS, $ids);
  641. }
  642. }
  643. // Remove tag id lists
  644. $this->_redis->del( $this->_preprocessTagIds($tags));
  645. // Remove tags from list of tags
  646. $this->_redis->sRem( self::SET_TAGS, $tags);
  647. $this->_redis->exec();
  648. }
  649. /**
  650. * Clean up tag id lists since as keys expire the ids remain in the tag id lists
  651. */
  652. protected function _collectGarbage()
  653. {
  654. // Clean up expired keys from tag id set and global id set
  655. if ($this->_useLua) {
  656. $sArgs = array(self::PREFIX_KEY, self::SET_TAGS, self::SET_IDS, self::PREFIX_TAG_IDS, ($this->_notMatchingTags ? 1 : 0));
  657. $allTags = (array) $this->_redis->sMembers(self::SET_TAGS);
  658. $tagsCount = count($allTags);
  659. $counter = 0;
  660. $tagsBatch = array();
  661. foreach ($allTags as $tag) {
  662. $tagsBatch[] = $tag;
  663. $counter++;
  664. if (count($tagsBatch) == 10 || $counter == $tagsCount ) {
  665. if ( ! $this->_redis->evalSha(self::LUA_GC_SH1, $tagsBatch, $sArgs)) {
  666. $script =
  667. "local tagKeys = {} ".
  668. "local expired = {} ".
  669. "local expiredCount = 0 ".
  670. "local notExpiredCount = 0 ".
  671. "for _, tagName in ipairs(KEYS) do ".
  672. "tagKeys = redis.call('SMEMBERS', ARGV[4]..tagName) ".
  673. "for __, keyName in ipairs(tagKeys) do ".
  674. "if (redis.call('EXISTS', ARGV[1]..keyName) == 0) then ".
  675. "expiredCount = expiredCount + 1 ".
  676. "expired[expiredCount] = keyName ".
  677. /* Redis Lua scripts have a hard limit of 8000 parameters per command */
  678. "if (expiredCount == 7990) then ".
  679. "redis.call('SREM', ARGV[4]..tagName, unpack(expired)) ".
  680. "if (ARGV[5] == '1') then ".
  681. "redis.call('SREM', ARGV[3], unpack(expired)) ".
  682. "end ".
  683. "expiredCount = 0 ".
  684. "expired = {} ".
  685. "end ".
  686. "else ".
  687. "notExpiredCount = notExpiredCount + 1 ".
  688. "end ".
  689. "end ".
  690. "if (expiredCount > 0) then ".
  691. "redis.call('SREM', ARGV[4]..tagName, unpack(expired)) ".
  692. "if (ARGV[5] == '1') then ".
  693. "redis.call('SREM', ARGV[3], unpack(expired)) ".
  694. "end ".
  695. "end ".
  696. "if (notExpiredCount == 0) then ".
  697. "redis.call ('DEL', ARGV[4]..tagName) ".
  698. "redis.call ('SREM', ARGV[2], tagName) ".
  699. "end ".
  700. "expired = {} ".
  701. "expiredCount = 0 ".
  702. "notExpiredCount = 0 ".
  703. "end ".
  704. "return true";
  705. $this->_redis->eval($script, $tagsBatch, $sArgs);
  706. }
  707. $tagsBatch = array();
  708. /* Give Redis some time to handle other requests */
  709. usleep(20000);
  710. }
  711. }
  712. return;
  713. }
  714. $exists = array();
  715. $tags = (array) $this->_redis->sMembers(self::SET_TAGS);
  716. foreach($tags as $tag)
  717. {
  718. // Get list of expired ids for each tag
  719. $tagMembers = $this->_redis->sMembers(self::PREFIX_TAG_IDS . $tag);
  720. $numTagMembers = count($tagMembers);
  721. $expired = array();
  722. $numExpired = $numNotExpired = 0;
  723. if($numTagMembers) {
  724. while ($id = array_pop($tagMembers)) {
  725. if( ! isset($exists[$id])) {
  726. $exists[$id] = $this->_redis->exists(self::PREFIX_KEY.$id);
  727. }
  728. if ($exists[$id]) {
  729. $numNotExpired++;
  730. }
  731. else {
  732. $numExpired++;
  733. $expired[] = $id;
  734. // Remove incrementally to reduce memory usage
  735. if (count($expired) % 100 == 0 && $numNotExpired > 0) {
  736. $this->_redis->sRem( self::PREFIX_TAG_IDS . $tag, $expired);
  737. if($this->_notMatchingTags) { // Clean up expired ids from ids set
  738. $this->_redis->sRem( self::SET_IDS, $expired);
  739. }
  740. $expired = array();
  741. }
  742. }
  743. }
  744. if( ! count($expired)) continue;
  745. }
  746. // Remove empty tags or completely expired tags
  747. if ($numExpired == $numTagMembers) {
  748. $this->_redis->del(self::PREFIX_TAG_IDS . $tag);
  749. $this->_redis->sRem(self::SET_TAGS, $tag);
  750. }
  751. // Clean up expired ids from tag ids set
  752. else if (count($expired)) {
  753. $this->_redis->sRem( self::PREFIX_TAG_IDS . $tag, $expired);
  754. if($this->_notMatchingTags) { // Clean up expired ids from ids set
  755. $this->_redis->sRem( self::SET_IDS, $expired);
  756. }
  757. }
  758. unset($expired);
  759. }
  760. // Clean up global list of ids for ids with no tag
  761. if($this->_notMatchingTags) {
  762. // TODO
  763. }
  764. }
  765. /**
  766. * Clean some cache records
  767. *
  768. * Available modes are :
  769. * 'all' (default) => remove all cache entries ($tags is not used)
  770. * 'old' => runs _collectGarbage()
  771. * 'matchingTag' => supported
  772. * 'notMatchingTag' => supported
  773. * 'matchingAnyTag' => supported
  774. *
  775. * @param string $mode Clean mode
  776. * @param array $tags Array of tags
  777. * @throws Zend_Cache_Exception
  778. * @return boolean True if no problem
  779. */
  780. public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
  781. {
  782. if( $tags && ! is_array($tags)) {
  783. $tags = array($tags);
  784. }
  785. try {
  786. if ($mode == Zend_Cache::CLEANING_MODE_ALL) {
  787. return $this->_redis->flushDb();
  788. }
  789. if ($mode == Zend_Cache::CLEANING_MODE_OLD) {
  790. $this->_collectGarbage();
  791. return TRUE;
  792. }
  793. if ( ! count($tags)) {
  794. return TRUE;
  795. }
  796. switch ($mode)
  797. {
  798. case Zend_Cache::CLEANING_MODE_MATCHING_TAG:
  799. $this->_removeByMatchingTags($tags);
  800. break;
  801. case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
  802. $this->_removeByNotMatchingTags($tags);
  803. break;
  804. case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
  805. $this->_removeByMatchingAnyTags($tags);
  806. break;
  807. default:
  808. Zend_Cache::throwException('Invalid mode for clean() method: '.$mode);
  809. }
  810. } catch (CredisException $e) {
  811. Zend_Cache::throwException('Error cleaning cache by mode '.$mode.': '.$e->getMessage(), $e);
  812. }
  813. return TRUE;
  814. }
  815. /**
  816. * Return true if the automatic cleaning is available for the backend
  817. *
  818. * @return boolean
  819. */
  820. public function isAutomaticCleaningAvailable()
  821. {
  822. return TRUE;
  823. }
  824. /**
  825. * Set the frontend directives
  826. *
  827. * @param array $directives Assoc of directives
  828. * @throws Zend_Cache_Exception
  829. * @return void
  830. */
  831. public function setDirectives($directives)
  832. {
  833. parent::setDirectives($directives);
  834. $lifetime = $this->getLifetime(false);
  835. if ($lifetime > self::MAX_LIFETIME) {
  836. Zend_Cache::throwException('Redis backend has a limit of 30 days (2592000 seconds) for the lifetime');
  837. }
  838. }
  839. /**
  840. * Get the auto expiring lifetime.
  841. *
  842. * Mainly a workaround for the issues that arise due to the fact that
  843. * Magento's Enterprise_PageCache module doesn't set any expiry.
  844. *
  845. * @param int $specificLifetime
  846. * @param string $id
  847. * @return int Cache life time
  848. */
  849. protected function _getAutoExpiringLifetime($lifetime, $id)
  850. {
  851. if ($lifetime || !$this->_autoExpireLifetime) {
  852. // If it's already truthy, or there's no auto expire go with it.
  853. return $lifetime;
  854. }
  855. $matches = $this->_matchesAutoExpiringPattern($id);
  856. if (!$matches) {
  857. // Only apply auto expire for keys that match the pattern
  858. return $lifetime;
  859. }
  860. if ($this->_autoExpireLifetime > 0) {
  861. // Return the auto expire lifetime if set
  862. return $this->_autoExpireLifetime;
  863. }
  864. // Return whatever it was set to.
  865. return $lifetime;
  866. }
  867. protected function _matchesAutoExpiringPattern($id)
  868. {
  869. $matches = array();
  870. preg_match($this->_autoExpirePattern, $id, $matches);
  871. return !empty($matches);
  872. }
  873. /**
  874. * Return an array of stored cache ids
  875. *
  876. * @return array array of stored cache ids (string)
  877. */
  878. public function getIds()
  879. {
  880. if($this->_notMatchingTags) {
  881. return (array) $this->_redis->sMembers(self::SET_IDS);
  882. } else {
  883. $keys = $this->_redis->keys(self::PREFIX_KEY . '*');
  884. $prefixLen = strlen(self::PREFIX_KEY);
  885. foreach($keys as $index => $key) {
  886. $keys[$index] = substr($key, $prefixLen);
  887. }
  888. return $keys;
  889. }
  890. }
  891. /**
  892. * Return an array of stored tags
  893. *
  894. * @return array array of stored tags (string)
  895. */
  896. public function getTags()
  897. {
  898. return (array) $this->_redis->sMembers(self::SET_TAGS);
  899. }
  900. /**
  901. * Return an array of stored cache ids which match given tags
  902. *
  903. * In case of multiple tags, a logical AND is made between tags
  904. *
  905. * @param array $tags array of tags
  906. * @return array array of matching cache ids (string)
  907. */
  908. public function getIdsMatchingTags($tags = array())
  909. {
  910. if ($tags) {
  911. return (array) $this->_redis->sInter( $this->_preprocessTagIds($tags) );
  912. }
  913. return array();
  914. }
  915. /**
  916. * Return an array of stored cache ids which don't match given tags
  917. *
  918. * In case of multiple tags, a negated logical AND is made between tags
  919. *
  920. * @param array $tags array of tags
  921. * @return array array of not matching cache ids (string)
  922. */
  923. public function getIdsNotMatchingTags($tags = array())
  924. {
  925. if( ! $this->_notMatchingTags) {
  926. Zend_Cache::throwException("notMatchingTags is currently disabled.");
  927. }
  928. if ($tags) {
  929. return (array) $this->_redis->sDiff( self::SET_IDS, $this->_preprocessTagIds($tags) );
  930. }
  931. return (array) $this->_redis->sMembers( self::SET_IDS );
  932. }
  933. /**
  934. * Return an array of stored cache ids which match any given tags
  935. *
  936. * In case of multiple tags, a logical OR is made between tags
  937. *
  938. * @param array $tags array of tags
  939. * @return array array of any matching cache ids (string)
  940. */
  941. public function getIdsMatchingAnyTags($tags = array())
  942. {
  943. $result = array();
  944. if ($tags) {
  945. $chunks = array_chunk($tags, $this->_sunionChunkSize);
  946. foreach ($chunks as $chunk) {
  947. $result = array_merge($result, (array) $this->_redis->sUnion( $this->_preprocessTagIds($chunk)));
  948. }
  949. if (count($chunks) > 1) {
  950. $result = array_unique($result); // since we are chunking requests, we must de-duplicate member names
  951. }
  952. }
  953. return $result;
  954. }
  955. /**
  956. * Return the filling percentage of the backend storage
  957. *
  958. * @throws Zend_Cache_Exception
  959. * @return int integer between 0 and 100
  960. */
  961. public function getFillingPercentage()
  962. {
  963. $maxMem = $this->_redis->config('GET','maxmemory');
  964. if (0 == (int) $maxMem['maxmemory']) {
  965. return 1;
  966. }
  967. $info = $this->_redis->info();
  968. return (int) round(
  969. ($info['used_memory']/$maxMem['maxmemory']*100)
  970. ,0
  971. ,PHP_ROUND_HALF_UP
  972. );
  973. }
  974. /**
  975. * Return an array of metadatas for the given cache id
  976. *
  977. * The array must include these keys :
  978. * - expire : the expire timestamp
  979. * - tags : a string array of tags
  980. * - mtime : timestamp of last modification time
  981. *
  982. * @param string $id cache id
  983. * @return array array of metadatas (false if the cache id is not found)
  984. */
  985. public function getMetadatas($id)
  986. {
  987. list($tags, $mtime, $inf) = array_values(
  988. $this->_redis->hMGet(self::PREFIX_KEY.$id, array(self::FIELD_TAGS, self::FIELD_MTIME, self::FIELD_INF))
  989. );
  990. if( ! $mtime) {
  991. return FALSE;
  992. }
  993. $tags = explode(',', $this->_decodeData($tags));
  994. $expire = $inf === '1' ? FALSE : time() + $this->_redis->ttl(self::PREFIX_KEY.$id);
  995. return array(
  996. 'expire' => $expire,
  997. 'tags' => $tags,
  998. 'mtime' => $mtime,
  999. );
  1000. }
  1001. /**
  1002. * Give (if possible) an extra lifetime to the given cache id
  1003. *
  1004. * @param string $id cache id
  1005. * @param int $extraLifetime
  1006. * @return boolean true if ok
  1007. */
  1008. public function touch($id, $extraLifetime)
  1009. {
  1010. list($inf) = $this->_redis->hGet(self::PREFIX_KEY.$id, self::FIELD_INF);
  1011. if ($inf === '0') {
  1012. $expireAt = time() + $this->_redis->ttl(self::PREFIX_KEY.$id) + $extraLifetime;
  1013. return (bool) $this->_redis->expireAt(self::PREFIX_KEY.$id, $expireAt);
  1014. }
  1015. return false;
  1016. }
  1017. /**
  1018. * Return an associative array of capabilities (booleans) of the backend
  1019. *
  1020. * The array must include these keys :
  1021. * - automatic_cleaning (is automating cleaning necessary)
  1022. * - tags (are tags supported)
  1023. * - expired_read (is it possible to read expired cache records
  1024. * (for doNotTestCacheValidity option for example))
  1025. * - priority does the backend deal with priority when saving
  1026. * - infinite_lifetime (is infinite lifetime can work with this backend)
  1027. * - get_list (is it possible to get the list of cache ids and the complete list of tags)
  1028. *
  1029. * @return array associative of with capabilities
  1030. */
  1031. public function getCapabilities()
  1032. {
  1033. return array(
  1034. 'automatic_cleaning' => ($this->_options['automatic_cleaning_factor'] > 0),
  1035. 'tags' => true,
  1036. 'expired_read' => false,
  1037. 'priority' => false,
  1038. 'infinite_lifetime' => true,
  1039. 'get_list' => true,
  1040. );
  1041. }
  1042. /**
  1043. * @param string $data
  1044. * @param int $level
  1045. * @throws CredisException
  1046. * @return string
  1047. */
  1048. protected function _encodeData($data, $level)
  1049. {
  1050. if ($this->_compressionLib && $level !== 0 && strlen($data) >= $this->_compressThreshold) {
  1051. switch($this->_compressionLib) {
  1052. case 'snappy': $data = snappy_compress($data); break;
  1053. case 'lzf': $data = lzf_compress($data); break;
  1054. case 'l4z': $data = lz4_compress($data, $level); break;
  1055. case 'zstd': $data = zstd_compress($data, $level); break;
  1056. case 'gzip': $data = gzcompress($data, $level); break;
  1057. default: throw new CredisException("Unrecognized 'compression_lib'.");
  1058. }
  1059. if( ! $data) {
  1060. throw new CredisException("Could not compress cache data.");
  1061. }
  1062. return $this->_compressPrefix.$data;
  1063. }
  1064. return $data;
  1065. }
  1066. /**
  1067. * @param bool|string $data
  1068. * @return string
  1069. */
  1070. protected function _decodeData($data)
  1071. {
  1072. if (substr($data,2,3) == self::COMPRESS_PREFIX) {
  1073. switch(substr($data,0,2)) {
  1074. case 'sn': return snappy_uncompress(substr($data,5));
  1075. case 'lz': return lzf_decompress(substr($data,5));
  1076. case 'l4': return lz4_uncompress(substr($data,5));
  1077. case 'zs': return zstd_uncompress(substr($data,5));
  1078. case 'gz': case 'zc': return gzuncompress(substr($data,5));
  1079. }
  1080. }
  1081. return $data;
  1082. }
  1083. /**
  1084. * @param $item
  1085. * @param $index
  1086. * @param $prefix
  1087. */
  1088. protected function _preprocess(&$item, $index, $prefix)
  1089. {
  1090. $item = $prefix . $item;
  1091. }
  1092. /**
  1093. * @param $ids
  1094. * @return array
  1095. */
  1096. protected function _preprocessIds($ids)
  1097. {
  1098. array_walk($ids, array($this, '_preprocess'), self::PREFIX_KEY);
  1099. return $ids;
  1100. }
  1101. /**
  1102. * @param $tags
  1103. * @return array
  1104. */
  1105. protected function _preprocessTagIds($tags)
  1106. {
  1107. array_walk($tags, array($this, '_preprocess'), self::PREFIX_TAG_IDS);
  1108. return $tags;
  1109. }
  1110. /**
  1111. * Required to pass unit tests
  1112. *
  1113. * @param string $id
  1114. * @return void
  1115. */
  1116. public function ___expire($id)
  1117. {
  1118. $this->_redis->del(self::PREFIX_KEY.$id);
  1119. }
  1120. /**
  1121. * Only for unit tests
  1122. */
  1123. public function ___scriptFlush()
  1124. {
  1125. $this->_redis->script('flush');
  1126. }
  1127. }