RedisStore.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <?php
  2. namespace Illuminate\Cache;
  3. use Illuminate\Contracts\Cache\LockProvider;
  4. use Illuminate\Contracts\Redis\Factory as Redis;
  5. use Illuminate\Redis\Connections\PhpRedisConnection;
  6. use Illuminate\Redis\Connections\PredisConnection;
  7. use Illuminate\Support\LazyCollection;
  8. use Illuminate\Support\Str;
  9. class RedisStore extends TaggableStore implements LockProvider
  10. {
  11. /**
  12. * The Redis factory implementation.
  13. *
  14. * @var \Illuminate\Contracts\Redis\Factory
  15. */
  16. protected $redis;
  17. /**
  18. * A string that should be prepended to keys.
  19. *
  20. * @var string
  21. */
  22. protected $prefix;
  23. /**
  24. * The Redis connection instance that should be used to manage locks.
  25. *
  26. * @var string
  27. */
  28. protected $connection;
  29. /**
  30. * The name of the connection that should be used for locks.
  31. *
  32. * @var string
  33. */
  34. protected $lockConnection;
  35. /**
  36. * Create a new Redis store.
  37. *
  38. * @param \Illuminate\Contracts\Redis\Factory $redis
  39. * @param string $prefix
  40. * @param string $connection
  41. * @return void
  42. */
  43. public function __construct(Redis $redis, $prefix = '', $connection = 'default')
  44. {
  45. $this->redis = $redis;
  46. $this->setPrefix($prefix);
  47. $this->setConnection($connection);
  48. }
  49. /**
  50. * Retrieve an item from the cache by key.
  51. *
  52. * @param string|array $key
  53. * @return mixed
  54. */
  55. public function get($key)
  56. {
  57. $value = $this->connection()->get($this->prefix.$key);
  58. return ! is_null($value) ? $this->unserialize($value) : null;
  59. }
  60. /**
  61. * Retrieve multiple items from the cache by key.
  62. *
  63. * Items not found in the cache will have a null value.
  64. *
  65. * @param array $keys
  66. * @return array
  67. */
  68. public function many(array $keys)
  69. {
  70. if (count($keys) === 0) {
  71. return [];
  72. }
  73. $results = [];
  74. $values = $this->connection()->mget(array_map(function ($key) {
  75. return $this->prefix.$key;
  76. }, $keys));
  77. foreach ($values as $index => $value) {
  78. $results[$keys[$index]] = ! is_null($value) ? $this->unserialize($value) : null;
  79. }
  80. return $results;
  81. }
  82. /**
  83. * Store an item in the cache for a given number of seconds.
  84. *
  85. * @param string $key
  86. * @param mixed $value
  87. * @param int $seconds
  88. * @return bool
  89. */
  90. public function put($key, $value, $seconds)
  91. {
  92. return (bool) $this->connection()->setex(
  93. $this->prefix.$key, (int) max(1, $seconds), $this->serialize($value)
  94. );
  95. }
  96. /**
  97. * Store multiple items in the cache for a given number of seconds.
  98. *
  99. * @param array $values
  100. * @param int $seconds
  101. * @return bool
  102. */
  103. public function putMany(array $values, $seconds)
  104. {
  105. $serializedValues = [];
  106. foreach ($values as $key => $value) {
  107. $serializedValues[$this->prefix.$key] = $this->serialize($value);
  108. }
  109. $this->connection()->multi();
  110. $manyResult = null;
  111. foreach ($serializedValues as $key => $value) {
  112. $result = (bool) $this->connection()->setex(
  113. $key, (int) max(1, $seconds), $value
  114. );
  115. $manyResult = is_null($manyResult) ? $result : $result && $manyResult;
  116. }
  117. $this->connection()->exec();
  118. return $manyResult ?: false;
  119. }
  120. /**
  121. * Store an item in the cache if the key doesn't exist.
  122. *
  123. * @param string $key
  124. * @param mixed $value
  125. * @param int $seconds
  126. * @return bool
  127. */
  128. public function add($key, $value, $seconds)
  129. {
  130. $lua = "return redis.call('exists',KEYS[1])<1 and redis.call('setex',KEYS[1],ARGV[2],ARGV[1])";
  131. return (bool) $this->connection()->eval(
  132. $lua, 1, $this->prefix.$key, $this->serialize($value), (int) max(1, $seconds)
  133. );
  134. }
  135. /**
  136. * Increment the value of an item in the cache.
  137. *
  138. * @param string $key
  139. * @param mixed $value
  140. * @return int
  141. */
  142. public function increment($key, $value = 1)
  143. {
  144. return $this->connection()->incrby($this->prefix.$key, $value);
  145. }
  146. /**
  147. * Decrement the value of an item in the cache.
  148. *
  149. * @param string $key
  150. * @param mixed $value
  151. * @return int
  152. */
  153. public function decrement($key, $value = 1)
  154. {
  155. return $this->connection()->decrby($this->prefix.$key, $value);
  156. }
  157. /**
  158. * Store an item in the cache indefinitely.
  159. *
  160. * @param string $key
  161. * @param mixed $value
  162. * @return bool
  163. */
  164. public function forever($key, $value)
  165. {
  166. return (bool) $this->connection()->set($this->prefix.$key, $this->serialize($value));
  167. }
  168. /**
  169. * Get a lock instance.
  170. *
  171. * @param string $name
  172. * @param int $seconds
  173. * @param string|null $owner
  174. * @return \Illuminate\Contracts\Cache\Lock
  175. */
  176. public function lock($name, $seconds = 0, $owner = null)
  177. {
  178. $lockName = $this->prefix.$name;
  179. $lockConnection = $this->lockConnection();
  180. if ($lockConnection instanceof PhpRedisConnection) {
  181. return new PhpRedisLock($lockConnection, $lockName, $seconds, $owner);
  182. }
  183. return new RedisLock($lockConnection, $lockName, $seconds, $owner);
  184. }
  185. /**
  186. * Restore a lock instance using the owner identifier.
  187. *
  188. * @param string $name
  189. * @param string $owner
  190. * @return \Illuminate\Contracts\Cache\Lock
  191. */
  192. public function restoreLock($name, $owner)
  193. {
  194. return $this->lock($name, 0, $owner);
  195. }
  196. /**
  197. * Remove an item from the cache.
  198. *
  199. * @param string $key
  200. * @return bool
  201. */
  202. public function forget($key)
  203. {
  204. return (bool) $this->connection()->del($this->prefix.$key);
  205. }
  206. /**
  207. * Remove all items from the cache.
  208. *
  209. * @return bool
  210. */
  211. public function flush()
  212. {
  213. $this->connection()->flushdb();
  214. return true;
  215. }
  216. /**
  217. * Remove all expired tag set entries.
  218. *
  219. * @return void
  220. */
  221. public function flushStaleTags()
  222. {
  223. foreach ($this->currentTags()->chunk(1000) as $tags) {
  224. $this->tags($tags->all())->flushStale();
  225. }
  226. }
  227. /**
  228. * Begin executing a new tags operation.
  229. *
  230. * @param array|mixed $names
  231. * @return \Illuminate\Cache\RedisTaggedCache
  232. */
  233. public function tags($names)
  234. {
  235. return new RedisTaggedCache(
  236. $this, new RedisTagSet($this, is_array($names) ? $names : func_get_args())
  237. );
  238. }
  239. /**
  240. * Get a collection of all of the cache tags currently being used.
  241. *
  242. * @param int $chunkSize
  243. * @return \Illuminate\Support\LazyCollection
  244. */
  245. protected function currentTags($chunkSize = 1000)
  246. {
  247. $connection = $this->connection();
  248. // Connections can have a global prefix...
  249. $connectionPrefix = match (true) {
  250. $connection instanceof PhpRedisConnection => $connection->_prefix(''),
  251. $connection instanceof PredisConnection => $connection->getOptions()->prefix ?: '',
  252. default => '',
  253. };
  254. $prefix = $connectionPrefix.$this->getPrefix();
  255. return LazyCollection::make(function () use ($connection, $chunkSize, $prefix) {
  256. $cursor = $defaultCursorValue = '0';
  257. do {
  258. [$cursor, $tagsChunk] = $connection->scan(
  259. $cursor,
  260. ['match' => $prefix.'tag:*:entries', 'count' => $chunkSize]
  261. );
  262. if (! is_array($tagsChunk)) {
  263. break;
  264. }
  265. $tagsChunk = array_unique($tagsChunk);
  266. if (empty($tagsChunk)) {
  267. continue;
  268. }
  269. foreach ($tagsChunk as $tag) {
  270. yield $tag;
  271. }
  272. } while (((string) $cursor) !== $defaultCursorValue);
  273. })->map(fn (string $tagKey) => Str::match('/^'.preg_quote($prefix, '/').'tag:(.*):entries$/', $tagKey));
  274. }
  275. /**
  276. * Get the Redis connection instance.
  277. *
  278. * @return \Illuminate\Redis\Connections\Connection
  279. */
  280. public function connection()
  281. {
  282. return $this->redis->connection($this->connection);
  283. }
  284. /**
  285. * Get the Redis connection instance that should be used to manage locks.
  286. *
  287. * @return \Illuminate\Redis\Connections\Connection
  288. */
  289. public function lockConnection()
  290. {
  291. return $this->redis->connection($this->lockConnection ?? $this->connection);
  292. }
  293. /**
  294. * Specify the name of the connection that should be used to store data.
  295. *
  296. * @param string $connection
  297. * @return void
  298. */
  299. public function setConnection($connection)
  300. {
  301. $this->connection = $connection;
  302. }
  303. /**
  304. * Specify the name of the connection that should be used to manage locks.
  305. *
  306. * @param string $connection
  307. * @return $this
  308. */
  309. public function setLockConnection($connection)
  310. {
  311. $this->lockConnection = $connection;
  312. return $this;
  313. }
  314. /**
  315. * Get the Redis database instance.
  316. *
  317. * @return \Illuminate\Contracts\Redis\Factory
  318. */
  319. public function getRedis()
  320. {
  321. return $this->redis;
  322. }
  323. /**
  324. * Get the cache key prefix.
  325. *
  326. * @return string
  327. */
  328. public function getPrefix()
  329. {
  330. return $this->prefix;
  331. }
  332. /**
  333. * Set the cache key prefix.
  334. *
  335. * @param string $prefix
  336. * @return void
  337. */
  338. public function setPrefix($prefix)
  339. {
  340. $this->prefix = ! empty($prefix) ? $prefix.':' : '';
  341. }
  342. /**
  343. * Serialize the value.
  344. *
  345. * @param mixed $value
  346. * @return mixed
  347. */
  348. protected function serialize($value)
  349. {
  350. return is_numeric($value) && ! in_array($value, [INF, -INF]) && ! is_nan($value) ? $value : serialize($value);
  351. }
  352. /**
  353. * Unserialize the value.
  354. *
  355. * @param mixed $value
  356. * @return mixed
  357. */
  358. protected function unserialize($value)
  359. {
  360. return is_numeric($value) ? $value : unserialize($value);
  361. }
  362. }