RedisEngine.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 2.2.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Cache\Engine;
  17. use Cake\Cache\CacheEngine;
  18. use Redis;
  19. use RedisException;
  20. /**
  21. * Redis storage engine for cache.
  22. */
  23. class RedisEngine extends CacheEngine
  24. {
  25. /**
  26. * Redis wrapper.
  27. *
  28. * @var \Redis
  29. */
  30. protected $_Redis;
  31. /**
  32. * The default config used unless overridden by runtime configuration
  33. *
  34. * - `database` database number to use for connection.
  35. * - `duration` Specify how long items in this cache configuration last.
  36. * - `groups` List of groups or 'tags' associated to every key stored in this config.
  37. * handy for deleting a complete group from cache.
  38. * - `password` Redis server password.
  39. * - `persistent` Connect to the Redis server with a persistent connection
  40. * - `port` port number to the Redis server.
  41. * - `prefix` Prefix appended to all entries. Good for when you need to share a keyspace
  42. * with either another cache config or another application.
  43. * - `server` URL or ip to the Redis server host.
  44. * - `timeout` timeout in seconds (float).
  45. * - `unix_socket` Path to the unix socket file (default: false)
  46. *
  47. * @var array
  48. */
  49. protected $_defaultConfig = [
  50. 'database' => 0,
  51. 'duration' => 3600,
  52. 'groups' => [],
  53. 'password' => false,
  54. 'persistent' => true,
  55. 'port' => 6379,
  56. 'prefix' => 'cake_',
  57. 'host' => null,
  58. 'server' => '127.0.0.1',
  59. 'timeout' => 0,
  60. 'unix_socket' => false,
  61. ];
  62. /**
  63. * Initialize the Cache Engine
  64. *
  65. * Called automatically by the cache frontend
  66. *
  67. * @param array $config array of setting for the engine
  68. * @return bool True if the engine has been successfully initialized, false if not
  69. */
  70. public function init(array $config = []): bool
  71. {
  72. if (!extension_loaded('redis')) {
  73. return false;
  74. }
  75. if (!empty($config['host'])) {
  76. $config['server'] = $config['host'];
  77. }
  78. parent::init($config);
  79. return $this->_connect();
  80. }
  81. /**
  82. * Connects to a Redis server
  83. *
  84. * @return bool True if Redis server was connected
  85. */
  86. protected function _connect(): bool
  87. {
  88. try {
  89. $this->_Redis = new Redis();
  90. if (!empty($this->_config['unix_socket'])) {
  91. $return = $this->_Redis->connect($this->_config['unix_socket']);
  92. } elseif (empty($this->_config['persistent'])) {
  93. $return = $this->_Redis->connect(
  94. $this->_config['server'],
  95. $this->_config['port'],
  96. $this->_config['timeout']
  97. );
  98. } else {
  99. $persistentId = $this->_config['port'] . $this->_config['timeout'] . $this->_config['database'];
  100. $return = $this->_Redis->pconnect(
  101. $this->_config['server'],
  102. (int)$this->_config['port'],
  103. (int)$this->_config['timeout'],
  104. $persistentId
  105. );
  106. }
  107. } catch (RedisException $e) {
  108. return false;
  109. }
  110. if ($return && $this->_config['password']) {
  111. $return = $this->_Redis->auth($this->_config['password']);
  112. }
  113. if ($return) {
  114. $return = $this->_Redis->select((int)$this->_config['database']);
  115. }
  116. return $return;
  117. }
  118. /**
  119. * Write data for key into cache.
  120. *
  121. * @param string $key Identifier for the data
  122. * @param mixed $value Data to be cached
  123. * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
  124. * the driver supports TTL then the library may set a default value
  125. * for it or let the driver take care of that.
  126. * @return bool True if the data was successfully cached, false on failure
  127. */
  128. public function set($key, $value, $ttl = null): bool
  129. {
  130. $key = $this->_key($key);
  131. if (!is_int($value)) {
  132. $value = serialize($value);
  133. }
  134. $duration = $this->duration($ttl);
  135. if ($duration === 0) {
  136. return $this->_Redis->set($key, $value);
  137. }
  138. return $this->_Redis->setEx($key, $duration, $value);
  139. }
  140. /**
  141. * Read a key from the cache
  142. *
  143. * @param string $key Identifier for the data
  144. * @param mixed $default Default value to return if the key does not exist.
  145. * @return mixed The cached data, or the default if the data doesn't exist, has
  146. * expired, or if there was an error fetching it
  147. */
  148. public function get($key, $default = null)
  149. {
  150. $value = $this->_Redis->get($this->_key($key));
  151. if ($value === false) {
  152. return $default;
  153. }
  154. if (preg_match('/^[-]?\d+$/', $value)) {
  155. return (int)$value;
  156. }
  157. return unserialize($value);
  158. }
  159. /**
  160. * Increments the value of an integer cached key & update the expiry time
  161. *
  162. * @param string $key Identifier for the data
  163. * @param int $offset How much to increment
  164. * @return int|false New incremented value, false otherwise
  165. */
  166. public function increment(string $key, int $offset = 1)
  167. {
  168. $duration = $this->_config['duration'];
  169. $key = $this->_key($key);
  170. $value = (int)$this->_Redis->incrBy($key, $offset);
  171. if ($duration > 0) {
  172. $this->_Redis->expire($key, $duration);
  173. }
  174. return $value;
  175. }
  176. /**
  177. * Decrements the value of an integer cached key & update the expiry time
  178. *
  179. * @param string $key Identifier for the data
  180. * @param int $offset How much to subtract
  181. * @return int|false New decremented value, false otherwise
  182. */
  183. public function decrement(string $key, int $offset = 1)
  184. {
  185. $duration = $this->_config['duration'];
  186. $key = $this->_key($key);
  187. $value = (int)$this->_Redis->decrBy($key, $offset);
  188. if ($duration > 0) {
  189. $this->_Redis->expire($key, $duration);
  190. }
  191. return $value;
  192. }
  193. /**
  194. * Delete a key from the cache
  195. *
  196. * @param string $key Identifier for the data
  197. * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed
  198. */
  199. public function delete($key): bool
  200. {
  201. $key = $this->_key($key);
  202. return $this->_Redis->del($key) > 0;
  203. }
  204. /**
  205. * Delete all keys from the cache
  206. *
  207. * @return bool True if the cache was successfully cleared, false otherwise
  208. */
  209. public function clear(): bool
  210. {
  211. $this->_Redis->setOption(Redis::OPT_SCAN, (string)Redis::SCAN_RETRY);
  212. $isAllDeleted = true;
  213. $iterator = null;
  214. $pattern = $this->_config['prefix'] . '*';
  215. while (true) {
  216. $keys = $this->_Redis->scan($iterator, $pattern);
  217. if ($keys === false) {
  218. break;
  219. }
  220. foreach ($keys as $key) {
  221. $isDeleted = ($this->_Redis->del($key) > 0);
  222. $isAllDeleted = $isAllDeleted && $isDeleted;
  223. }
  224. }
  225. return $isAllDeleted;
  226. }
  227. /**
  228. * Write data for key into cache if it doesn't exist already.
  229. * If it already exists, it fails and returns false.
  230. *
  231. * @param string $key Identifier for the data.
  232. * @param mixed $value Data to be cached.
  233. * @return bool True if the data was successfully cached, false on failure.
  234. * @link https://github.com/phpredis/phpredis#setnx
  235. */
  236. public function add(string $key, $value): bool
  237. {
  238. $duration = $this->_config['duration'];
  239. $key = $this->_key($key);
  240. if (!is_int($value)) {
  241. $value = serialize($value);
  242. }
  243. // setNx() doesn't have an expiry option, so follow up with an expiry
  244. if ($this->_Redis->setNx($key, $value)) {
  245. return $this->_Redis->expire($key, $duration);
  246. }
  247. return false;
  248. }
  249. /**
  250. * Returns the `group value` for each of the configured groups
  251. * If the group initial value was not found, then it initializes
  252. * the group accordingly.
  253. *
  254. * @return string[]
  255. */
  256. public function groups(): array
  257. {
  258. $result = [];
  259. foreach ($this->_config['groups'] as $group) {
  260. $value = $this->_Redis->get($this->_config['prefix'] . $group);
  261. if (!$value) {
  262. $value = 1;
  263. $this->_Redis->set($this->_config['prefix'] . $group, $value);
  264. }
  265. $result[] = $group . $value;
  266. }
  267. return $result;
  268. }
  269. /**
  270. * Increments the group value to simulate deletion of all keys under a group
  271. * old values will remain in storage until they expire.
  272. *
  273. * @param string $group name of the group to be cleared
  274. * @return bool success
  275. */
  276. public function clearGroup(string $group): bool
  277. {
  278. return (bool)$this->_Redis->incr($this->_config['prefix'] . $group);
  279. }
  280. /**
  281. * Disconnects from the redis server
  282. */
  283. public function __destruct()
  284. {
  285. if (empty($this->_config['persistent']) && $this->_Redis instanceof Redis) {
  286. $this->_Redis->close();
  287. }
  288. }
  289. }