1<?php 2 3namespace Doctrine\Common\Cache; 4 5use Redis; 6use function array_combine; 7use function array_diff_key; 8use function array_fill_keys; 9use function array_filter; 10use function array_keys; 11use function count; 12use function defined; 13use function extension_loaded; 14use function is_bool; 15 16/** 17 * Redis cache provider. 18 * 19 * @link www.doctrine-project.org 20 */ 21class RedisCache extends CacheProvider 22{ 23 /** @var Redis|null */ 24 private $redis; 25 26 /** 27 * Sets the redis instance to use. 28 * 29 * @return void 30 */ 31 public function setRedis(Redis $redis) 32 { 33 $redis->setOption(Redis::OPT_SERIALIZER, $this->getSerializerValue()); 34 $this->redis = $redis; 35 } 36 37 /** 38 * Gets the redis instance used by the cache. 39 * 40 * @return Redis|null 41 */ 42 public function getRedis() 43 { 44 return $this->redis; 45 } 46 47 /** 48 * {@inheritdoc} 49 */ 50 protected function doFetch($id) 51 { 52 return $this->redis->get($id); 53 } 54 55 /** 56 * {@inheritdoc} 57 */ 58 protected function doFetchMultiple(array $keys) 59 { 60 $fetchedItems = array_combine($keys, $this->redis->mget($keys)); 61 62 // Redis mget returns false for keys that do not exist. So we need to filter those out unless it's the real data. 63 $keysToFilter = array_keys(array_filter($fetchedItems, static function ($item) : bool { 64 return $item === false; 65 })); 66 67 if ($keysToFilter) { 68 $multi = $this->redis->multi(Redis::PIPELINE); 69 foreach ($keysToFilter as $key) { 70 $multi->exists($key); 71 } 72 $existItems = array_filter($multi->exec()); 73 $missedItemKeys = array_diff_key($keysToFilter, $existItems); 74 $fetchedItems = array_diff_key($fetchedItems, array_fill_keys($missedItemKeys, true)); 75 } 76 77 return $fetchedItems; 78 } 79 80 /** 81 * {@inheritdoc} 82 */ 83 protected function doSaveMultiple(array $keysAndValues, $lifetime = 0) 84 { 85 if ($lifetime) { 86 // Keys have lifetime, use SETEX for each of them 87 $multi = $this->redis->multi(Redis::PIPELINE); 88 foreach ($keysAndValues as $key => $value) { 89 $multi->setex($key, $lifetime, $value); 90 } 91 $succeeded = array_filter($multi->exec()); 92 93 return count($succeeded) == count($keysAndValues); 94 } 95 96 // No lifetime, use MSET 97 return (bool) $this->redis->mset($keysAndValues); 98 } 99 100 /** 101 * {@inheritdoc} 102 */ 103 protected function doContains($id) 104 { 105 $exists = $this->redis->exists($id); 106 107 if (is_bool($exists)) { 108 return $exists; 109 } 110 111 return $exists > 0; 112 } 113 114 /** 115 * {@inheritdoc} 116 */ 117 protected function doSave($id, $data, $lifeTime = 0) 118 { 119 if ($lifeTime > 0) { 120 return $this->redis->setex($id, $lifeTime, $data); 121 } 122 123 return $this->redis->set($id, $data); 124 } 125 126 /** 127 * {@inheritdoc} 128 */ 129 protected function doDelete($id) 130 { 131 return $this->redis->del($id) >= 0; 132 } 133 134 /** 135 * {@inheritdoc} 136 */ 137 protected function doDeleteMultiple(array $keys) 138 { 139 return $this->redis->del($keys) >= 0; 140 } 141 142 /** 143 * {@inheritdoc} 144 */ 145 protected function doFlush() 146 { 147 return $this->redis->flushDB(); 148 } 149 150 /** 151 * {@inheritdoc} 152 */ 153 protected function doGetStats() 154 { 155 $info = $this->redis->info(); 156 157 return [ 158 Cache::STATS_HITS => $info['keyspace_hits'], 159 Cache::STATS_MISSES => $info['keyspace_misses'], 160 Cache::STATS_UPTIME => $info['uptime_in_seconds'], 161 Cache::STATS_MEMORY_USAGE => $info['used_memory'], 162 Cache::STATS_MEMORY_AVAILABLE => false, 163 ]; 164 } 165 166 /** 167 * Returns the serializer constant to use. If Redis is compiled with 168 * igbinary support, that is used. Otherwise the default PHP serializer is 169 * used. 170 * 171 * @return int One of the Redis::SERIALIZER_* constants 172 */ 173 protected function getSerializerValue() 174 { 175 if (defined('Redis::SERIALIZER_IGBINARY') && extension_loaded('igbinary')) { 176 return Redis::SERIALIZER_IGBINARY; 177 } 178 179 return Redis::SERIALIZER_PHP; 180 } 181} 182