From a7bb060bbd423d053795fdc907a9342cc0f41cd8 Mon Sep 17 00:00:00 2001 From: Sasha Rybkin Date: Fri, 12 Sep 2025 13:45:10 +0300 Subject: [PATCH] =?UTF-8?q?#117067=20=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D1=80=D0=B5=D0=B4=D0=B8=D1=81=20=D0=BD?= =?UTF-8?q?=D0=B0=20yii=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=D1=85?= =?UTF-8?q?=20RedisSentinel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Connection.php | 402 +++++++++++++++++++++++++++++++++++------ RedisCache.php | 31 ++-- SentinelConnection.php | 148 +++++++++++++++ Session.php | 172 ++++++++++++++++++ composer.json | 4 +- 5 files changed, 685 insertions(+), 72 deletions(-) create mode 100644 SentinelConnection.php create mode 100644 Session.php diff --git a/Connection.php b/Connection.php index b9fd373..3e992e8 100644 --- a/Connection.php +++ b/Connection.php @@ -3,84 +3,384 @@ namespace dominion\cache; use Yii; -use Predis\Client; +use yii\base\Component; use yii\helpers\Inflector; +use Redis; -class Connection extends \yii\redis\Connection +/** + * Работа череез однк ноду + * пример конфига + * + * [ + * 'class' => 'dominion\cache\Connection', + * 'parameters' => [ + * 'host' => 'node1.redis.service.optiweb', + * 'port' => '6379', + * 'prefix' => "prefix", + * 'username' => '', + * 'password' => 'password', + * 'database' => 0, + * 'read_timeout' => 0, + * 'scan' => , + * 'name' => , + * 'serializer' => , + * 'compression' =>, + * 'compression_level' => + * ] + * ] + */ +class Connection extends Component { - /** - * @var mixed Connection parameters for one or more servers. - */ - public $parameters; - /** - * @var mixed Options to configure some behaviours of the client. - */ - public $options = []; + public $parameters = []; /** - * @var Client redis socket connection + * @var array List of available redis commands. + * @see https://redis.io/commands */ - private $_socket = false; + public $redisCommands = [ + 'APPEND', // Append a value to a key + 'AUTH', // Authenticate to the server + 'BGREWRITEAOF', // Asynchronously rewrite the append-only file + 'BGSAVE', // Asynchronously save the dataset to disk + 'BITCOUNT', // Count set bits in a string + 'BITFIELD', // Perform arbitrary bitfield integer operations on strings + 'BITOP', // Perform bitwise operations between strings + 'BITPOS', // Find first bit set or clear in a string + 'BLPOP', // Remove and get the first element in a list, or block until one is available + 'BRPOP', // Remove and get the last element in a list, or block until one is available + 'BRPOPLPUSH', // Pop a value from a list, push it to another list and return it; or block until one is available + 'CLIENT KILL', // Kill the connection of a client + 'CLIENT LIST', // Get the list of client connections + 'CLIENT GETNAME', // Get the current connection name + 'CLIENT PAUSE', // Stop processing commands from clients for some time + 'CLIENT REPLY', // Instruct the server whether to reply to commands + 'CLIENT SETNAME', // Set the current connection name + 'CLUSTER ADDSLOTS', // Assign new hash slots to receiving node + 'CLUSTER COUNTKEYSINSLOT', // Return the number of local keys in the specified hash slot + 'CLUSTER DELSLOTS', // Set hash slots as unbound in receiving node + 'CLUSTER FAILOVER', // Forces a slave to perform a manual failover of its master. + 'CLUSTER FORGET', // Remove a node from the nodes table + 'CLUSTER GETKEYSINSLOT', // Return local key names in the specified hash slot + 'CLUSTER INFO', // Provides info about Redis Cluster node state + 'CLUSTER KEYSLOT', // Returns the hash slot of the specified key + 'CLUSTER MEET', // Force a node cluster to handshake with another node + 'CLUSTER NODES', // Get Cluster config for the node + 'CLUSTER REPLICATE', // Reconfigure a node as a slave of the specified master node + 'CLUSTER RESET', // Reset a Redis Cluster node + 'CLUSTER SAVECONFIG', // Forces the node to save cluster state on disk + 'CLUSTER SETSLOT', // Bind a hash slot to a specific node + 'CLUSTER SLAVES', // List slave nodes of the specified master node + 'CLUSTER SLOTS', // Get array of Cluster slot to node mappings + 'COMMAND', // Get array of Redis command details + 'COMMAND COUNT', // Get total number of Redis commands + 'COMMAND GETKEYS', // Extract keys given a full Redis command + 'COMMAND INFO', // Get array of specific Redis command details + 'CONFIG GET', // Get the value of a configuration parameter + 'CONFIG REWRITE', // Rewrite the configuration file with the in memory configuration + 'CONFIG SET', // Set a configuration parameter to the given value + 'CONFIG RESETSTAT', // Reset the stats returned by INFO + 'DBSIZE', // Return the number of keys in the selected database + 'DEBUG OBJECT', // Get debugging information about a key + 'DEBUG SEGFAULT', // Make the server crash + 'DECR', // Decrement the integer value of a key by one + 'DECRBY', // Decrement the integer value of a key by the given number + 'DEL', // Delete a key + 'DISCARD', // Discard all commands issued after MULTI + 'DUMP', // Return a serialized version of the value stored at the specified key. + 'ECHO', // Echo the given string + 'EVAL', // Execute a Lua script server side + 'EVALSHA', // Execute a Lua script server side + 'EXEC', // Execute all commands issued after MULTI + 'EXISTS', // Determine if a key exists + 'EXPIRE', // Set a key's time to live in seconds + 'EXPIREAT', // Set the expiration for a key as a UNIX timestamp + 'FLUSHALL', // Remove all keys from all databases + 'FLUSHDB', // Remove all keys from the current database + 'GEOADD', // Add one or more geospatial items in the geospatial index represented using a sorted set + 'GEOHASH', // Returns members of a geospatial index as standard geohash strings + 'GEOPOS', // Returns longitude and latitude of members of a geospatial index + 'GEODIST', // Returns the distance between two members of a geospatial index + 'GEORADIUS', // Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point + 'GEORADIUSBYMEMBER', // Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member + 'GET', // Get the value of a key + 'GETBIT', // Returns the bit value at offset in the string value stored at key + 'GETRANGE', // Get a substring of the string stored at a key + 'GETSET', // Set the string value of a key and return its old value + 'HDEL', // Delete one or more hash fields + 'HEXISTS', // Determine if a hash field exists + 'HGET', // Get the value of a hash field + 'HGETALL', // Get all the fields and values in a hash + 'HINCRBY', // Increment the integer value of a hash field by the given number + 'HINCRBYFLOAT', // Increment the float value of a hash field by the given amount + 'HKEYS', // Get all the fields in a hash + 'HLEN', // Get the number of fields in a hash + 'HMGET', // Get the values of all the given hash fields + 'HMSET', // Set multiple hash fields to multiple values + 'HSET', // Set the string value of a hash field + 'HSETNX', // Set the value of a hash field, only if the field does not exist + 'HSTRLEN', // Get the length of the value of a hash field + 'HVALS', // Get all the values in a hash + 'INCR', // Increment the integer value of a key by one + 'INCRBY', // Increment the integer value of a key by the given amount + 'INCRBYFLOAT', // Increment the float value of a key by the given amount + 'INFO', // Get information and statistics about the server + 'KEYS', // Find all keys matching the given pattern + 'LASTSAVE', // Get the UNIX time stamp of the last successful save to disk + 'LINDEX', // Get an element from a list by its index + 'LINSERT', // Insert an element before or after another element in a list + 'LLEN', // Get the length of a list + 'LPOP', // Remove and get the first element in a list + 'LPUSH', // Prepend one or multiple values to a list + 'LPUSHX', // Prepend a value to a list, only if the list exists + 'LRANGE', // Get a range of elements from a list + 'LREM', // Remove elements from a list + 'LSET', // Set the value of an element in a list by its index + 'LTRIM', // Trim a list to the specified range + 'MGET', // Get the values of all the given keys + 'MIGRATE', // Atomically transfer a key from a Redis instance to another one. + 'MONITOR', // Listen for all requests received by the server in real time + 'MOVE', // Move a key to another database + 'MSET', // Set multiple keys to multiple values + 'MSETNX', // Set multiple keys to multiple values, only if none of the keys exist + 'MULTI', // Mark the start of a transaction block + 'OBJECT', // Inspect the internals of Redis objects + 'PERSIST', // Remove the expiration from a key + 'PEXPIRE', // Set a key's time to live in milliseconds + 'PEXPIREAT', // Set the expiration for a key as a UNIX timestamp specified in milliseconds + 'PFADD', // Adds the specified elements to the specified HyperLogLog. + 'PFCOUNT', // Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s). + 'PFMERGE', // Merge N different HyperLogLogs into a single one. + 'PING', // Ping the server + 'PSETEX', // Set the value and expiration in milliseconds of a key + 'PSUBSCRIBE', // Listen for messages published to channels matching the given patterns + 'PUBSUB', // Inspect the state of the Pub/Sub subsystem + 'PTTL', // Get the time to live for a key in milliseconds + 'PUBLISH', // Post a message to a channel + 'PUNSUBSCRIBE', // Stop listening for messages posted to channels matching the given patterns + 'QUIT', // Close the connection + 'RANDOMKEY', // Return a random key from the keyspace + 'READONLY', // Enables read queries for a connection to a cluster slave node + 'READWRITE', // Disables read queries for a connection to a cluster slave node + 'RENAME', // Rename a key + 'RENAMENX', // Rename a key, only if the new key does not exist + 'RESTORE', // Create a key using the provided serialized value, previously obtained using DUMP. + 'ROLE', // Return the role of the instance in the context of replication + 'RPOP', // Remove and get the last element in a list + 'RPOPLPUSH', // Remove the last element in a list, prepend it to another list and return it + 'RPUSH', // Append one or multiple values to a list + 'RPUSHX', // Append a value to a list, only if the list exists + 'SADD', // Add one or more members to a set + 'SAVE', // Synchronously save the dataset to disk + 'SCARD', // Get the number of members in a set + 'SCRIPT DEBUG', // Set the debug mode for executed scripts. + 'SCRIPT EXISTS', // Check existence of scripts in the script cache. + 'SCRIPT FLUSH', // Remove all the scripts from the script cache. + 'SCRIPT KILL', // Kill the script currently in execution. + 'SCRIPT LOAD', // Load the specified Lua script into the script cache. + 'SDIFF', // Subtract multiple sets + 'SDIFFSTORE', // Subtract multiple sets and store the resulting set in a key + 'SELECT', // Change the selected database for the current connection + 'SET', // Set the string value of a key + 'SETBIT', // Sets or clears the bit at offset in the string value stored at key + 'SETEX', // Set the value and expiration of a key + 'SETNX', // Set the value of a key, only if the key does not exist + 'SETRANGE', // Overwrite part of a string at key starting at the specified offset + 'SHUTDOWN', // Synchronously save the dataset to disk and then shut down the server + 'SINTER', // Intersect multiple sets + 'SINTERSTORE', // Intersect multiple sets and store the resulting set in a key + 'SISMEMBER', // Determine if a given value is a member of a set + 'SLAVEOF', // Make the server a slave of another instance, or promote it as master + 'SLOWLOG', // Manages the Redis slow queries log + 'SMEMBERS', // Get all the members in a set + 'SMOVE', // Move a member from one set to another + 'SORT', // Sort the elements in a list, set or sorted set + 'SPOP', // Remove and return one or multiple random members from a set + 'SRANDMEMBER', // Get one or multiple random members from a set + 'SREM', // Remove one or more members from a set + 'STRLEN', // Get the length of the value stored in a key + 'SUBSCRIBE', // Listen for messages published to the given channels + 'SUNION', // Add multiple sets + 'SUNIONSTORE', // Add multiple sets and store the resulting set in a key + 'SWAPDB', // Swaps two Redis databases + 'SYNC', // Internal command used for replication + 'TIME', // Return the current server time + 'TOUCH', // Alters the last access time of a key(s). Returns the number of existing keys specified. + 'TTL', // Get the time to live for a key + 'TYPE', // Determine the type stored at key + 'UNSUBSCRIBE', // Stop listening for messages posted to the given channels + 'UNLINK', // Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking. + 'UNWATCH', // Forget about all watched keys + 'WAIT', // Wait for the synchronous replication of all the write commands sent in the context of the current connection + 'WATCH', // Watch the given keys to determine execution of the MULTI/EXEC block + 'XACK', // Removes one or multiple messages from the pending entries list (PEL) of a stream consumer group + 'XADD', // Appends the specified stream entry to the stream at the specified key + 'XCLAIM', // Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument + 'XDEL', // Removes the specified entries from a stream, and returns the number of entries deleted + 'XGROUP', // Manages the consumer groups associated with a stream data structure + 'XINFO', // Retrieves different information about the streams and associated consumer groups + 'XLEN', // Returns the number of entries inside a stream + 'XPENDING', // Fetching data from a stream via a consumer group, and not acknowledging such data, has the effect of creating pending entries + 'XRANGE', // Returns the stream entries matching a given range of IDs + 'XREAD', // Read data from one or multiple streams, only returning entries with an ID greater than the last received ID reported by the caller + 'XREADGROUP', // Special version of the XREAD command with support for consumer groups + 'XREVRANGE', // Exactly like XRANGE, but with the notable difference of returning the entries in reverse order, and also taking the start-end range in reverse order + 'XTRIM', // Trims the stream to a given number of items, evicting older items (items with lower IDs) if needed + 'ZADD', // Add one or more members to a sorted set, or update its score if it already exists + 'ZCARD', // Get the number of members in a sorted set + 'ZCOUNT', // Count the members in a sorted set with scores within the given values + 'ZINCRBY', // Increment the score of a member in a sorted set + 'ZINTERSTORE', // Intersect multiple sorted sets and store the resulting sorted set in a new key + 'ZLEXCOUNT', // Count the number of members in a sorted set between a given lexicographical range + 'ZRANGE', // Return a range of members in a sorted set, by index + 'ZRANGEBYLEX', // Return a range of members in a sorted set, by lexicographical range + 'ZREVRANGEBYLEX', // Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings. + 'ZRANGEBYSCORE', // Return a range of members in a sorted set, by score + 'ZRANK', // Determine the index of a member in a sorted set + 'ZREM', // Remove one or more members from a sorted set + 'ZREMRANGEBYLEX', // Remove all members in a sorted set between the given lexicographical range + 'ZREMRANGEBYRANK', // Remove all members in a sorted set within the given indexes + 'ZREMRANGEBYSCORE', // Remove all members in a sorted set within the given scores + 'ZREVRANGE', // Return a range of members in a sorted set, by index, with scores ordered from high to low + 'ZREVRANGEBYSCORE', // Return a range of members in a sorted set, by score, with scores ordered from high to low + 'ZREVRANK', // Determine the index of a member in a sorted set, with scores ordered from high to low + 'ZSCORE', // Get the score associated with the given member in a sorted set + 'ZUNIONSTORE', // Add multiple sorted sets and store the resulting sorted set in a new key + 'SCAN', // Incrementally iterate the keys space + 'SSCAN', // Incrementally iterate Set elements + 'HSCAN', // Incrementally iterate hash fields and associated values + 'ZSCAN', // Incrementally iterate sorted sets elements and associated scores + ]; - public function __call($name, $params) - { - $redisCommand = strtoupper(Inflector::camel2words($name, false)); - if (in_array($redisCommand, $this->redisCommands)) { - return $this->executeCommand($redisCommand, $params); - } else { - return parent::__call($name, $params); - } - } + public $redis = false; - public function executeCommand($name, $params = []) - { - $this->open(); - Yii::debug("Executing Redis Command: {$name} " . implode(' ', $params), __METHOD__); - return $this->_socket->executeCommand( - $this->_socket->createCommand($name, $params) - ); - } - - /** - * Closes the connection when this component is being serialized. - * @return array - */ public function __sleep() { $this->close(); return array_keys(get_object_vars($this)); } - /** - * Returns a value indicating whether the DB connection is established. - * @return bool whether the DB connection is established - */ public function getIsActive() { - return $this->_socket !== false; + return $this->redis !== false; } public function open() { - if ($this->_socket !== false) { - return; + if (!$this->isActive) + { + Yii::debug('Opening redis DB connection: ' . var_export($this->parameters, true), __METHOD__); + $this->redis = $this->createClient($this->parameters); } - Yii::debug('Opening redis DB connection: ' /*. var_export($this->parameters, true)*/, __METHOD__); - $this->_socket = new Client($this->parameters, $this->options); - $this->initConnection(); } - /** - * Closes the currently active DB connection. - * It does nothing if the connection is already closed. - */ public function close() { - if ($this->_socket !== false) { + if ($this->isActive) + { Yii::debug('Closing DB connection: ' . var_export($this->parameters, true), __METHOD__); - $this->_socket->disconnect(); - $this->_socket = false; + $this->redis->disconnect(); + $this->redis = false; } } + public function __call($name, $params) + { + $redisCommand = strtoupper(Inflector::camel2words($name, false)); + if (in_array($redisCommand, $this->redisCommands)) + { + return $this->executeCommand($redisCommand, $params); + } + + return parent::__call($name, $params); + } + + public function executeCommand($name, $params = []) + { + $this->open(); + Yii::debug("Executing Redis Command: {$name} " . var_export($params, true), __METHOD__); + return $this->redis->{$name}(...$params); + } + + public function createClient(array $config): Redis + { + $client = new Redis; + $this->establishConnection($client, $config); + if (!empty($config['password'])) + { + if (isset($config['username']) && $config['username'] !== '' && is_string($config['password'])) + { + $client->auth([$config['username'], $config['password']]); + } + else + { + $client->auth($config['password']); + } + } + + if (isset($config['database'])) + { + $client->select((int) $config['database']); + } + + if (!empty($config['prefix'])) + { + $client->setOption(Redis::OPT_PREFIX, $config['prefix']); + } + + if (!empty($config['read_timeout'])) + { + $client->setOption(Redis::OPT_READ_TIMEOUT, $config['read_timeout']); + } + + if (!empty($config['scan'])) + { + $client->setOption(Redis::OPT_SCAN, $config['scan']); + } + + if (!empty($config['name'])) + { + $client->client('SETNAME', $config['name']); + } + + if (array_key_exists('serializer', $config)) + { + $client->setOption(Redis::OPT_SERIALIZER, $config['serializer']); + } + + if (array_key_exists('compression', $config)) + { + $client->setOption(Redis::OPT_COMPRESSION, $config['compression']); + } + + if (array_key_exists('compression_level', $config)) + { + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, $config['compression_level']); + } + return $client; + } + + protected function establishConnection($client, array $config) + { + $persistent = $config['persistent'] ?? false; + + $parameters = [ + $config['host'], + $config['port'], + isset($config['timeout']) ? $config['timeout'] : 0.0, + $persistent ? (isset($config['persistent_id']) ? $config['persistent_id'] : null) : null, + isset($config['retry_interval']) ? $config['retry_interval'] : 0, + ]; + + if (version_compare(phpversion('redis'), '3.1.3', '>=')) + { + $parameters[] = isset($config['read_timeout']) ? $config['read_timeout'] : 0.0; + } + + if (version_compare(phpversion('redis'), '5.3.0', '>=') && !is_null($context = isset($config['context']) ? $config['context'] : null)) + { + $parameters[] = $context; + } + + $client->{$persistent ? 'pconnect' : 'connect'}(...$parameters); + } } diff --git a/RedisCache.php b/RedisCache.php index 89fd857..8c984ee 100644 --- a/RedisCache.php +++ b/RedisCache.php @@ -52,7 +52,7 @@ class RedisCache { if (!self::$livetime) { - self::$livetime = isset(Yii::$app->params['redis'], Yii::$app->params['redis']['livetime']) ? (int) Yii::$app->params['redis']['livetime'] : 300; + self::$livetime = isset(Yii::$app->params['redis'], Yii::$app->params['redis']['livetime']) ? (int) Yii::$app->params['redis']['livetime'] : 15*60; } return self::$livetime; } @@ -61,16 +61,11 @@ class RedisCache { if (!self::$prefix) { - self::$prefix = isset(Yii::$app->params['redis'], Yii::$app->params['redis']['prefix']) ? (string)Yii::$app->params['redis']['prefix'] : ''; + self::$prefix = isset(Yii::$app->redis->parameters, Yii::$app->redis->parameters['prefix']) ? Yii::$app->redis->parameters['prefix'] : ''; } return self::$prefix; } - protected static function calculateKey($key) - { - return self::prefix() . $key; - } - public static function hdel($key, $value) { if (self::$setDeleteKey) @@ -79,7 +74,7 @@ class RedisCache } else { - Yii::$app->redis->hdel(self::calculateKey($key), $value); + Yii::$app->redis->hdel($key, $value); } } @@ -91,13 +86,13 @@ class RedisCache } else { - Yii::$app->redis->del(self::calculateKey($key)); + Yii::$app->redis->del($key); } } public static function hset($key, $field, $value) { - return self::getActive() ? Yii::$app->redis->hset(self::calculateKey($key), $field, json_encode($value)) : false; + return self::getActive() ? Yii::$app->redis->hset($key, $field, json_encode($value)) : false; } public static function hsetModel($key, $field, $model) @@ -137,7 +132,7 @@ class RedisCache $output = false; if(self::getActive()) { - $output = json_decode(Yii::$app->redis->hget(self::calculateKey($key), $field), true); + $output = json_decode(Yii::$app->redis->hget($key, $field), true); } return $output === null ? false : $output;; } @@ -201,7 +196,7 @@ class RedisCache foreach ($patterns as $pattern) { $arKeys = []; - while ($values = Yii::$app->redis->hscan(self::calculateKey($key), $cursor, $pattern)) + while ($values = Yii::$app->redis->hscan($key, $cursor, $pattern)) { foreach ($values as $vkey => $value) { @@ -233,7 +228,7 @@ class RedisCache public static function getKeyTypes() { - $result = Yii::$app->redis->keys(self::calculateKey('*')); + $result = Yii::$app->redis->keys('*'); $output = ['*']; foreach ($result as $val) { @@ -258,7 +253,7 @@ class RedisCache { self::deleteAll($type); $type = $type == "*" ? "*" : "{$type}:*"; - $result = Yii::$app->redis->keys(self::calculateKey($type)); + $result = Yii::$app->redis->keys($type); foreach ($result as $val) { self::deleteAll(str_replace(self::prefix(), '', $val)); @@ -280,7 +275,7 @@ class RedisCache { $options = ['EX' => $options]; } - Yii::$app->redis->set(self::calculateKey($key), json_encode($value) .' '. implode(' ', $options)); + Yii::$app->redis->set($key, json_encode($value), $options); } return $result; } @@ -313,7 +308,7 @@ class RedisCache $result = false; if (self::getActive()) { - $result = json_decode(Yii::$app->redis->get(self::calculateKey($key)), true); + $result = json_decode(Yii::$app->redis->get($key), true); } return $result === null ? false : $result; } @@ -380,14 +375,14 @@ class RedisCache */ public static function oldCacheDelete() { - $result = Yii::$app->redis->keys(self::calculateKey('del:*')); + $result = Yii::$app->redis->keys('del:*'); foreach ($result as $val) { $cursor = 0;//null; $key = str_replace(self::prefix(), '', $val); $pattern = '*'; $arKeys = []; - while ($values = Yii::$app->redis->hscan(self::calculateKey($key), $cursor, $pattern)) + while ($values = Yii::$app->redis->hscan($key, $cursor, $pattern)) { foreach ($values as $vkey => $value) { diff --git a/SentinelConnection.php b/SentinelConnection.php new file mode 100644 index 0000000..dd2f53b --- /dev/null +++ b/SentinelConnection.php @@ -0,0 +1,148 @@ +open(); + Yii::debug("Executing Redis Command: {$name} " . var_export($params, true), __METHOD__); + + $redis = $this->redis; //мастер + $redisCommand = strtoupper(Inflector::camel2words($name, false)); + if (in_array($redisCommand, $this->redisSlaveCommands)) + { + if(!empty($this->slaves)) + { + //читаем из произвольного раба + $redis = $this->slaves[array_rand($this->slaves)]; + } + } + + return $redis->{$name}(...$params); + } + + protected function isValidServer(mixed $server): bool + { + return is_array($server) && isset($server['ip']) && isset($server['port']); + } + + public function createClient(array $config): Redis + { + $service = $config['sentinel_service'] ?? 'mymaster'; + + $hosts = is_array($config['sentinel_host']) ? $config['sentinel_host'] : [$config['sentinel_host']]; + foreach ($hosts as $host) + { + $newConfig = $config; + $newConfig['sentinel_host'] = $host; + $sentinel = $this->connectToSentinel($newConfig); + + if ($sentinel->ping()) + { + break; + } + } + $master = $sentinel->master($service); + if (!$this->isValidServer($master)) + { + throw new RedisException(sprintf("No master found for service '%s'.", $service)); + } + $slaves = $sentinel->slaves($service); + foreach ($slaves as $slave) + { + if ($this->isValidServer($slave)) + { + //могут быть проблемв из-за большого количества подключений + $this->slaves[] = parent::createClient(array_merge($config, [ + 'host' => $slave['ip'], + 'port' => $slave['port'], + ])); + } + } + + return parent::createClient(array_merge($config, [ + 'host' => $master['ip'], + 'port' => $master['port'], + ])); + } + + private function connectToSentinel(array $config): RedisSentinel + { + $host = $config['sentinel_host'] ?? ''; + $port = $config['sentinel_port'] ?? 26379; + $timeout = $config['sentinel_timeout'] ?? 0.2; + $persistent = $config['sentinel_persistent'] ?? null; + $retryInterval = $config['sentinel_retry_interval'] ?? 0; + $readTimeout = $config['sentinel_read_timeout'] ?? 0; + $username = $config['sentinel_username'] ?? ''; + $password = $config['sentinel_password'] ?? ''; + $ssl = $config['sentinel_ssl'] ?? null; + + if (strlen(trim($host)) === 0) + { + throw new ConfigurationException('No host has been specified for the Redis Sentinel connection.'); + } + + $auth = null; + if (strlen(trim($username)) !== 0 && strlen(trim($password)) !== 0) + { + $auth = [$username, $password]; + } + elseif (strlen(trim($password)) !== 0) + { + $auth = $password; + } + + if (version_compare(phpversion('redis'), '6.0', '>=')) + { + $options = [ + 'host' => $host, + 'port' => $port, + 'connectTimeout' => $timeout, + 'persistent' => $persistent, + 'retryInterval' => $retryInterval, + 'readTimeout' => $readTimeout, + ]; + + if ($auth !== null) + { + $options['auth'] = $auth; + } + + if (version_compare(phpversion('redis'), '6.1', '>=') && $ssl !== null) + { + $options['ssl'] = $ssl; + } + + return new RedisSentinel($options); + } + + if ($auth !== null) + { + /** @noinspection PhpMethodParametersCountMismatchInspection */ + return new RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout, $auth); + } + + return new RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout); + } +} diff --git a/Session.php b/Session.php new file mode 100644 index 0000000..307caa1 --- /dev/null +++ b/Session.php @@ -0,0 +1,172 @@ + [ + * 'session' => [ + * 'class' => 'yii\redis\Session', + * 'redis' => [ + * 'hostname' => 'localhost', + * 'port' => 6379, + * 'database' => 0, + * ] + * ], + * ], + * ] + * ~~~ + * + * Or if you have configured the redis [[Connection]] as an application component, the following is sufficient: + * + * ~~~ + * [ + * 'components' => [ + * 'session' => [ + * 'class' => 'yii\redis\Session', + * // 'redis' => 'redis' // id of the connection application component + * ], + * ], + * ] + * ~~~ + * + * @property-read bool $useCustomStorage Whether to use custom storage. + * + * @author Carsten Brandt + * @since 2.0 + */ +class Session extends \yii\web\Session +{ + /** + * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. + * This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure + * redis connection as an application component. + * After the Session object is created, if you want to change this property, you should only assign it + * with a Redis [[Connection]] object. + */ + public $redis = 'redis'; + /** + * @var string a string prefixed to every cache key so that it is unique. If not set, + * it will use a prefix generated from [[Application::id]]. You may set this property to be an empty string + * if you don't want to use key prefix. It is recommended that you explicitly set this property to some + * static value if the cached data needs to be shared among multiple applications. + */ + public $keyPrefix; + + + /** + * Initializes the redis Session component. + * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection. + * @throws InvalidConfigException if [[redis]] is invalid. + */ + public function init() + { + $this->redis = Instance::ensure($this->redis, Connection::className()); + if ($this->keyPrefix === null) { + $this->keyPrefix = substr(md5(Yii::$app->id), 0, 5); + } + parent::init(); + } + + /** + * Returns a value indicating whether to use custom session storage. + * This method overrides the parent implementation and always returns true. + * @return bool whether to use custom storage. + */ + public function getUseCustomStorage() + { + return true; + } + + /** + * Session open handler. + * @internal Do not call this method directly. + * @param string $savePath session save path + * @param string $sessionName session name + * @return bool whether session is opened successfully + */ + public function openSession($savePath, $sessionName) + { + if ($this->getUseStrictMode()) { + $id = $this->getId(); + if (!$this->redis->exists($this->calculateKey($id))) { + //This session id does not exist, mark it for forced regeneration + $this->_forceRegenerateId = $id; + } + } + + return parent::openSession($savePath, $sessionName); + } + + /** + * Session read handler. + * Do not call this method directly. + * @param string $id session ID + * @return string the session data + */ + public function readSession($id) + { + $data = $this->redis->get($this->calculateKey($id)); + + return $data === false || $data === null ? '' : $data; + } + + /** + * Session write handler. + * Do not call this method directly. + * @param string $id session ID + * @param string $data session data + * @return bool whether session write is successful + */ + public function writeSession($id, $data) + { + if ($this->getUseStrictMode() && $id === $this->_forceRegenerateId) { + //Ignore write when forceRegenerate is active for this id + return true; + } + + return (bool) $this->redis->set($this->calculateKey($id), $data, ['EX' => $this->getTimeout()]); + } + + /** + * Session destroy handler. + * Do not call this method directly. + * @param string $id session ID + * @return bool whether session is destroyed successfully + */ + public function destroySession($id) + { + $this->redis->del($this->calculateKey($id)); + // @see https://github.com/yiisoft/yii2-redis/issues/82 + return true; + } + + /** + * Generates a unique key used for storing session data in cache. + * @param string $id session variable name + * @return string a safe cache key associated with the session variable name + */ + protected function calculateKey($id) + { + return $this->keyPrefix . md5(json_encode([__CLASS__, $id])); + } +} diff --git a/composer.json b/composer.json index 1c6d9d1..e4900c6 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,7 @@ } ], "require": { - "yiisoft/yii2-redis": "~2.0.0", - "yiisoft/yii2": "~2.0.0", - "predis/predis": "^3.0.1" + "yiisoft/yii2": "~2.0.0" }, "autoload": { "psr-4": {