RedisConnection.php 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\Redis;
  12. use Hyperf\Contract\ConnectionInterface;
  13. use Hyperf\Contract\PoolInterface;
  14. use Hyperf\Contract\StdoutLoggerInterface;
  15. use Hyperf\Pool\Connection as BaseConnection;
  16. use Hyperf\Pool\Exception\ConnectionException;
  17. use Hyperf\Redis\Exception\InvalidRedisConnectionException;
  18. use Hyperf\Redis\Exception\InvalidRedisOptionException;
  19. use Psr\Container\ContainerInterface;
  20. use Psr\Log\LogLevel;
  21. use Redis;
  22. use RedisCluster;
  23. use RedisException;
  24. use Throwable;
  25. /**
  26. * @method bool select(int $db)
  27. */
  28. class RedisConnection extends BaseConnection implements ConnectionInterface
  29. {
  30. use Traits\ScanCaller;
  31. use Traits\MultiExec;
  32. protected null|Redis|RedisCluster $connection = null;
  33. protected array $config = [
  34. 'host' => 'localhost',
  35. 'port' => 6379,
  36. 'auth' => null,
  37. 'db' => 0,
  38. 'timeout' => 0.0,
  39. 'reserved' => null,
  40. 'retry_interval' => 0,
  41. 'read_timeout' => 0.0,
  42. 'cluster' => [
  43. 'enable' => false,
  44. 'name' => null,
  45. 'seeds' => [],
  46. 'read_timeout' => 0.0,
  47. 'persistent' => false,
  48. 'context' => [],
  49. ],
  50. 'sentinel' => [
  51. 'enable' => false,
  52. 'master_name' => '',
  53. 'nodes' => [],
  54. 'persistent' => '',
  55. 'read_timeout' => 0,
  56. ],
  57. 'options' => [],
  58. 'context' => [],
  59. ];
  60. /**
  61. * Current redis database.
  62. */
  63. protected ?int $database = null;
  64. public function __construct(ContainerInterface $container, PoolInterface $pool, array $config)
  65. {
  66. parent::__construct($container, $pool);
  67. $this->config = array_replace_recursive($this->config, $config);
  68. $this->reconnect();
  69. }
  70. public function __call($name, $arguments)
  71. {
  72. try {
  73. $result = $this->connection->{$name}(...$arguments);
  74. } catch (Throwable $exception) {
  75. $result = $this->retry($name, $arguments, $exception);
  76. }
  77. return $result;
  78. }
  79. public function getActiveConnection()
  80. {
  81. if ($this->check()) {
  82. return $this;
  83. }
  84. if (! $this->reconnect()) {
  85. throw new ConnectionException('Connection reconnect failed.');
  86. }
  87. return $this;
  88. }
  89. /**
  90. * @throws RedisException
  91. * @throws ConnectionException
  92. */
  93. public function reconnect(): bool
  94. {
  95. $auth = $this->config['auth'];
  96. $db = $this->config['db'];
  97. $cluster = $this->config['cluster']['enable'] ?? false;
  98. $sentinel = $this->config['sentinel']['enable'] ?? false;
  99. $redis = match (true) {
  100. $cluster => $this->createRedisCluster(),
  101. $sentinel => $this->createRedisSentinel(),
  102. default => $this->createRedis($this->config),
  103. };
  104. $options = $this->config['options'] ?? [];
  105. foreach ($options as $name => $value) {
  106. if (is_string($name)) {
  107. $name = match (strtolower($name)) {
  108. 'serializer' => Redis::OPT_SERIALIZER, // 1
  109. 'prefix' => Redis::OPT_PREFIX, // 2
  110. 'read_timeout' => Redis::OPT_READ_TIMEOUT, // 3
  111. 'scan' => Redis::OPT_SCAN, // 4
  112. 'failover' => defined(Redis::class . '::OPT_SLAVE_FAILOVER') ? Redis::OPT_SLAVE_FAILOVER : 5, // 5
  113. 'keepalive' => Redis::OPT_TCP_KEEPALIVE, // 6
  114. 'compression' => Redis::OPT_COMPRESSION, // 7
  115. 'reply_literal' => Redis::OPT_REPLY_LITERAL, // 8
  116. 'compression_level' => Redis::OPT_COMPRESSION_LEVEL, // 9
  117. default => throw new InvalidRedisOptionException(sprintf('The redis option key `%s` is invalid.', $name)),
  118. };
  119. }
  120. $redis->setOption($name, $value);
  121. }
  122. if ($redis instanceof Redis && isset($auth) && $auth !== '') {
  123. $redis->auth($auth);
  124. }
  125. $database = $this->database ?? $db;
  126. if ($database > 0) {
  127. $redis->select($database);
  128. }
  129. $this->connection = $redis;
  130. $this->lastUseTime = microtime(true);
  131. return true;
  132. }
  133. public function close(): bool
  134. {
  135. unset($this->connection);
  136. return true;
  137. }
  138. public function release(): void
  139. {
  140. try {
  141. if ($this->database && $this->database != $this->config['db']) {
  142. // Select the origin db after execute select.
  143. $this->select($this->config['db']);
  144. $this->database = null;
  145. }
  146. parent::release();
  147. } catch (Throwable $exception) {
  148. $this->log('Release connection failed, caused by ' . $exception, LogLevel::CRITICAL);
  149. }
  150. }
  151. public function setDatabase(?int $database): void
  152. {
  153. $this->database = $database;
  154. }
  155. protected function createRedisCluster(): RedisCluster
  156. {
  157. try {
  158. $parameters = [];
  159. $parameters[] = $this->config['cluster']['name'] ?? null;
  160. $parameters[] = $this->config['cluster']['seeds'] ?? [];
  161. $parameters[] = $this->config['timeout'] ?? 0.0;
  162. $parameters[] = $this->config['cluster']['read_timeout'] ?? 0.0;
  163. $parameters[] = $this->config['cluster']['persistent'] ?? false;
  164. if (isset($this->config['auth'])) {
  165. $parameters[] = $this->config['auth'];
  166. }
  167. if (! empty($this->config['cluster']['context'])) {
  168. $parameters[] = $this->config['cluster']['context'];
  169. }
  170. $redis = new RedisCluster(...$parameters);
  171. } catch (Throwable $e) {
  172. throw new ConnectionException('Connection reconnect failed ' . $e->getMessage());
  173. }
  174. return $redis;
  175. }
  176. protected function retry($name, $arguments, Throwable $exception)
  177. {
  178. $this->log('Redis::__call failed, because ' . $exception->getMessage());
  179. try {
  180. $this->reconnect();
  181. $result = $this->connection->{$name}(...$arguments);
  182. } catch (Throwable $exception) {
  183. $this->lastUseTime = 0.0;
  184. throw $exception;
  185. }
  186. return $result;
  187. }
  188. /**
  189. * @throws ConnectionException
  190. */
  191. protected function createRedisSentinel(): Redis
  192. {
  193. try {
  194. $nodes = $this->config['sentinel']['nodes'] ?? [];
  195. $timeout = $this->config['timeout'] ?? 0;
  196. $persistent = $this->config['sentinel']['persistent'] ?? null;
  197. $retryInterval = $this->config['retry_interval'] ?? 0;
  198. $readTimeout = $this->config['sentinel']['read_timeout'] ?? 0;
  199. $masterName = $this->config['sentinel']['master_name'] ?? '';
  200. $auth = $this->config['sentinel']['auth'] ?? null;
  201. shuffle($nodes);
  202. $host = null;
  203. $port = null;
  204. foreach ($nodes as $node) {
  205. try {
  206. $resolved = parse_url($node);
  207. if (! isset($resolved['host'], $resolved['port'])) {
  208. $this->log(sprintf('The redis sentinel node [%s] is invalid.', $node), LogLevel::ERROR);
  209. continue;
  210. }
  211. $options = [
  212. 'host' => $resolved['host'],
  213. 'port' => (int) $resolved['port'],
  214. 'connectTimeout' => $timeout,
  215. 'persistent' => $persistent,
  216. 'retryInterval' => $retryInterval,
  217. 'readTimeout' => $readTimeout,
  218. ...($auth ? ['auth' => $auth] : []),
  219. ];
  220. $sentinel = $this->container->get(RedisSentinelFactory::class)->create($options);
  221. $masterInfo = $sentinel->getMasterAddrByName($masterName);
  222. if (is_array($masterInfo) && count($masterInfo) >= 2) {
  223. [$host, $port] = $masterInfo;
  224. break;
  225. }
  226. } catch (Throwable $exception) {
  227. $this->log('Redis sentinel connection failed, caused by ' . $exception->getMessage());
  228. continue;
  229. }
  230. }
  231. if ($host === null && $port === null) {
  232. throw new InvalidRedisConnectionException('Connect sentinel redis server failed.');
  233. }
  234. $redis = $this->createRedis([
  235. 'host' => $host,
  236. 'port' => $port,
  237. 'timeout' => $timeout,
  238. 'retry_interval' => $retryInterval,
  239. 'read_timeout' => $readTimeout,
  240. ]);
  241. } catch (Throwable $e) {
  242. throw new ConnectionException('Connection reconnect failed ' . $e->getMessage());
  243. }
  244. return $redis;
  245. }
  246. /**
  247. * @throws ConnectionException
  248. * @throws RedisException
  249. */
  250. protected function createRedis(array $config): Redis
  251. {
  252. $parameters = [
  253. $config['host'] ?? '',
  254. (int) ($config['port'] ?? 6379),
  255. $config['timeout'] ?? 0.0,
  256. $config['reserved'] ?? null,
  257. $config['retry_interval'] ?? 0,
  258. $config['read_timeout'] ?? 0.0,
  259. ];
  260. if (! empty($config['context'])) {
  261. $parameters[] = $config['context'];
  262. }
  263. $redis = new Redis();
  264. if (! $redis->connect(...$parameters)) {
  265. throw new ConnectionException('Connection reconnect failed.');
  266. }
  267. return $redis;
  268. }
  269. private function log(string $message, string $level = LogLevel::WARNING): void
  270. {
  271. if ($this->container->has(StdoutLoggerInterface::class) && $logger = $this->container->get(StdoutLoggerInterface::class)) {
  272. $logger->log($level, $message);
  273. }
  274. }
  275. }