2025-06-10 20:14:43 +02:00
|
|
|
<?php
|
2025-07-27 19:58:08 +02:00
|
|
|
|
2025-06-10 20:14:43 +02:00
|
|
|
/**
|
|
|
|
|
* Credis, a Redis interface for the modest
|
|
|
|
|
*
|
|
|
|
|
* @author Justin Poliey <jdp34@njit.edu>
|
|
|
|
|
* @copyright 2009 Justin Poliey <jdp34@njit.edu>
|
|
|
|
|
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
|
|
|
|
|
* @package Credis
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
2025-07-27 19:58:08 +02:00
|
|
|
* Credis_Cluster, subclass to Credis_Client that uses RedisCluster (in phpredis extension)
|
2025-06-10 20:14:43 +02:00
|
|
|
*
|
2025-07-27 19:58:08 +02:00
|
|
|
* Note: RedisCluster currently has limitations like not supporting pipeline or multi.
|
|
|
|
|
* Note: Many methods require an additional parameter to specify which node to run on, and only run on that node,
|
|
|
|
|
* such as saveForNode(), flushDBForNode(), and pingForNode().
|
|
|
|
|
* Note: Redis clusters do not support select(), as they only have a single database.
|
|
|
|
|
* Note: RedisCluster currently has buggy/broken behaviour for pSubscribe and script.
|
2025-06-10 20:14:43 +02:00
|
|
|
*/
|
2025-07-27 19:58:08 +02:00
|
|
|
class Credis_Cluster extends Credis_Client
|
2025-06-10 20:14:43 +02:00
|
|
|
{
|
2025-07-27 19:58:08 +02:00
|
|
|
/**
|
|
|
|
|
* Name of the cluster as configured in redis.ini
|
|
|
|
|
* @var string|null
|
|
|
|
|
*/
|
|
|
|
|
protected $clusterName;
|
2025-06-10 20:14:43 +02:00
|
|
|
|
2025-07-27 19:58:08 +02:00
|
|
|
/**
|
|
|
|
|
* Hosts & ports of the cluster
|
|
|
|
|
* Eg: ['redis-node-1:6379', 'redis-node-2:6379', 'redis-node-3:6379', 'redis-node-4:6379']
|
|
|
|
|
* @var array|null
|
|
|
|
|
*/
|
|
|
|
|
protected $clusterSeeds;
|
2025-06-10 20:14:43 +02:00
|
|
|
|
2025-07-27 19:58:08 +02:00
|
|
|
/**
|
|
|
|
|
* Enable persistent connections
|
|
|
|
|
* @var bool
|
|
|
|
|
*/
|
|
|
|
|
protected $persistentBool;
|
2025-06-10 20:14:43 +02:00
|
|
|
|
2025-07-27 19:58:08 +02:00
|
|
|
/**
|
|
|
|
|
* Creates a connection to the Redis Cluster on cluser named {@link $clusterName} or seeds {@link $clusterSeeds}.
|
|
|
|
|
*
|
|
|
|
|
* @param string|null $clusterName Name of the cluster as configured in redis.ini
|
|
|
|
|
* @param array|null $clusterSeeds Hosts & ports of the cluster; eg: ['redis-node-1:6379', 'redis-node-2:6379']
|
|
|
|
|
* @param float|null $timeout Timeout period in seconds
|
|
|
|
|
* @param float|null $readTimeout Timeout period in seconds
|
|
|
|
|
* @param bool $persistentBool Flag to establish persistent connection
|
|
|
|
|
* @param string|null $password The authentication password of the Redis server
|
|
|
|
|
* @param string|null $username The authentication username of the Redis server
|
|
|
|
|
* @param array|null $tlsOptions If array, then uses TLS for non-seed connections; if null, no TLS for non-seed
|
|
|
|
|
* @throws CredisException
|
|
|
|
|
*/
|
|
|
|
|
public function __construct($clusterName = null, array $clusterSeeds = [], $timeout = null, $readTimeout = null, $persistentBool = false, $password = null, $username = null, $tlsOptions = null)
|
2025-06-10 20:14:43 +02:00
|
|
|
{
|
2025-07-27 19:58:08 +02:00
|
|
|
if (!class_exists(\RedisCluster::class)) {
|
|
|
|
|
throw new \Exception("Credis_Cluster depends on RedisCluster class from phpredis extension. "
|
|
|
|
|
. " Please verify that phpredis extension is installed and enabled");
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|
2025-07-27 19:58:08 +02:00
|
|
|
$this->clusterName = $clusterName;
|
|
|
|
|
$this->clusterSeeds = $clusterSeeds;
|
|
|
|
|
$this->scheme = null;
|
|
|
|
|
$this->timeout = $timeout;
|
|
|
|
|
$this->readTimeout = $readTimeout;
|
|
|
|
|
$this->persistentBool = $persistentBool;
|
|
|
|
|
$this->standalone = false;
|
|
|
|
|
$this->authPassword = $password;
|
|
|
|
|
$this->authUsername = $username;
|
|
|
|
|
$this->selectedDb = 0; // Note: Clusters don't have db, but it's in superclass
|
|
|
|
|
$this->tlsOptions = $tlsOptions;
|
|
|
|
|
// PHP Redis extension support TLS/ACL AUTH since 5.3.0 // Note: Do we need this in Credis_ClusterClient?
|
|
|
|
|
$this->oldPhpRedis = (bool)version_compare(phpversion('redis'), '5.3.0', '<');
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-27 19:58:08 +02:00
|
|
|
/**
|
|
|
|
|
* @inheritDoc
|
|
|
|
|
*/
|
|
|
|
|
public function connect()
|
|
|
|
|
{
|
|
|
|
|
if ($this->connected) {
|
|
|
|
|
return $this;
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|
2025-07-27 19:58:08 +02:00
|
|
|
$this->close(true);
|
|
|
|
|
if (!$this->redis) {
|
|
|
|
|
$this->redis = new RedisCluster(
|
|
|
|
|
$this->clusterName,
|
|
|
|
|
$this->clusterSeeds,
|
|
|
|
|
isset($this->timeout) ? $this->timeout : 0,
|
|
|
|
|
isset($this->readTimeout) ? $this->readTimeout : 0,
|
|
|
|
|
$this->persistentBool, // Note: This can't be $this->persistent, because it is string
|
|
|
|
|
['user' => $this->authUsername, 'pass' => $this->authPassword],
|
|
|
|
|
// Note: RedisCluster won't use TLS for non-seed connections if this is null
|
|
|
|
|
$this->tlsOptions
|
|
|
|
|
);
|
|
|
|
|
$this->connectFailures = 0;
|
|
|
|
|
$this->connected = true;
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string|null
|
|
|
|
|
*/
|
|
|
|
|
public function getClusterName()
|
|
|
|
|
{
|
|
|
|
|
return $this->clusterName;
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|
2025-07-27 19:58:08 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array|null
|
|
|
|
|
*/
|
|
|
|
|
public function getClusterSeeds()
|
|
|
|
|
{
|
|
|
|
|
return $this->clusterSeeds;
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|
2025-07-27 19:58:08 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function getPersistenceBool()
|
|
|
|
|
{
|
|
|
|
|
return $this->persistentBool;
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-27 19:58:08 +02:00
|
|
|
/*
|
|
|
|
|
* Gets list of masters from RedisCluster
|
|
|
|
|
*/
|
|
|
|
|
public function getClusterMasters(): array
|
|
|
|
|
{
|
|
|
|
|
$this->connect();
|
|
|
|
|
return $this->redis->_masters();
|
|
|
|
|
}
|
2025-06-10 20:14:43 +02:00
|
|
|
|
2025-07-27 19:58:08 +02:00
|
|
|
/**
|
|
|
|
|
* @inheritDoc
|
|
|
|
|
*
|
|
|
|
|
* Runs PING on all the "master" nodes.
|
|
|
|
|
*/
|
|
|
|
|
public function ping($message = null)
|
|
|
|
|
{
|
|
|
|
|
$this->connect();
|
|
|
|
|
foreach ($this->getClusterMasters() as $master) {
|
|
|
|
|
$response = $this->redis->ping($master, $message);
|
|
|
|
|
if (($response !== true) && (!is_string($response)) && ($response !== $this->redis)) {
|
|
|
|
|
return $output;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ($response === $this->redis) {
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
if ($response) {
|
|
|
|
|
if ($response === true) {
|
|
|
|
|
$response = isset($message) ? $message : "PONG";
|
|
|
|
|
} elseif ($response[0] === '+') {
|
|
|
|
|
$response = substr($response, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $response;
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-27 19:58:08 +02:00
|
|
|
/**
|
|
|
|
|
* @inheritDoc
|
|
|
|
|
*
|
|
|
|
|
* Runs FLUSHDB on all the "master" nodes.
|
|
|
|
|
*/
|
|
|
|
|
public function flushDb(...$args)
|
|
|
|
|
{
|
|
|
|
|
$this->connect();
|
|
|
|
|
foreach ($this->getClusterMasters() as $master) {
|
|
|
|
|
$output = $this->redis->flushDb($master, ...$args);
|
|
|
|
|
}
|
|
|
|
|
return $output;
|
|
|
|
|
}
|
2025-06-10 20:14:43 +02:00
|
|
|
|
|
|
|
|
|
2025-07-27 19:58:08 +02:00
|
|
|
/**
|
|
|
|
|
* @inheritDoc
|
|
|
|
|
*
|
|
|
|
|
* Runs FLUSHALL on all the "master" nodes.
|
|
|
|
|
*/
|
|
|
|
|
public function flushAll(...$args)
|
|
|
|
|
{
|
|
|
|
|
$this->connect();
|
|
|
|
|
foreach ($this->getClusterMasters() as $master) {
|
|
|
|
|
$output = $this->redis->flushAll($master, ...$args);
|
|
|
|
|
}
|
|
|
|
|
return $output;
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|
2025-07-27 19:58:08 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* To specify the node, the first argument will either be a key which maps to a slot which maps to a node; or it
|
|
|
|
|
* can be an array of ['host': port] for a node.
|
|
|
|
|
*
|
|
|
|
|
* @see flushAll
|
|
|
|
|
*/
|
|
|
|
|
public function flushAllForNode($node, ...$args)
|
|
|
|
|
{
|
|
|
|
|
$this->connect();
|
|
|
|
|
return $this->redis->flushAll($node, ...$args);
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|
2025-07-27 19:58:08 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* To specify the node, the first argument will either be a key which maps to a slot which maps to a node; or it
|
|
|
|
|
* can be an array of ['host': port] for a node.
|
|
|
|
|
*
|
|
|
|
|
* @see flushDb
|
|
|
|
|
*/
|
|
|
|
|
public function flushDbForNode($node, ...$args)
|
|
|
|
|
{
|
|
|
|
|
$this->connect();
|
|
|
|
|
return $this->redis->flushDb($node, ...$args);
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-27 19:58:08 +02:00
|
|
|
/**
|
|
|
|
|
* To specify the node, the first argument will either be a key which maps to a slot which maps to a node; or it
|
|
|
|
|
* can be an array of ['host': port] for a node.
|
|
|
|
|
*
|
|
|
|
|
* @see ping
|
|
|
|
|
*/
|
|
|
|
|
public function pingForNode($node, ...$args)
|
|
|
|
|
{
|
|
|
|
|
$this->connect();
|
|
|
|
|
$response = $this->redis->ping($node, ...$args);
|
|
|
|
|
if ($response === $this->redis) {
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
if ($response) {
|
|
|
|
|
if ($response === true) {
|
|
|
|
|
$response = isset($message) ? $message : "PONG";
|
|
|
|
|
} elseif ($response[0] === '+') {
|
|
|
|
|
$response = substr($response, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $response;
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-27 19:58:08 +02:00
|
|
|
/**
|
|
|
|
|
* To specify the node, the first argument will either be a key which maps to a slot which maps to a node; or it
|
|
|
|
|
* can be an array of ['host': port] for a node.
|
|
|
|
|
*
|
|
|
|
|
* @see save
|
|
|
|
|
*/
|
|
|
|
|
public function saveForNode($node, ...$args)
|
|
|
|
|
{
|
|
|
|
|
$this->connect();
|
|
|
|
|
return $this->redis->save($node, ...$args);
|
|
|
|
|
}
|
2025-06-10 20:14:43 +02:00
|
|
|
}
|