Skip to content

Commit f4930b4

Browse files
Adding support for RedisCluster (phpredis extension) (#191)
* Adding PHP 8.3 test container to run the phpunit tests * Updating Documentation for Credis_Client
1 parent 23428f3 commit f4930b4

16 files changed

+804
-715
lines changed

Cluster.php

Lines changed: 161 additions & 255 deletions
Large diffs are not rendered by default.

README.markdown

Lines changed: 20 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -78,146 +78,40 @@ $particles = $redis->lrange('particles', 0, -1);
7878

7979
## Clustering your servers
8080

81-
Credis also includes a way for developers to fully utilize the scalability of Redis with multiple servers and [consistent hashing](http://en.wikipedia.org/wiki/Consistent_hashing).
82-
Using the [Credis_Cluster](Cluster.php) class, you can use Credis the same way, except that keys will be hashed across multiple servers.
83-
Here is how to set up a cluster:
81+
Credis also includes a way for developers to fully utilize the [scalability of Redis cluster](https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/) by using Credis_Cluster which is an adapter for the RedisCluster class from [the Redis extension for PHP](https://github.com/phpredis/phpredis). This also works on [AWS ElastiCatch clusters](https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/Clusters.html).
82+
This feature requires the PHP extension for its functionality. Here is an example how to set up a cluster:
8483

8584
### Basic clustering example
8685
```php
8786
<?php
8887
require 'Credis/Client.php';
8988
require 'Credis/Cluster.php';
9089

91-
$cluster = new Credis_Cluster(array(
92-
array('host' => '127.0.0.1', 'port' => 6379, 'alias'=>'alpha'),
93-
array('host' => '127.0.0.1', 'port' => 6380, 'alias'=>'beta')
94-
));
95-
$cluster->set('key','value');
96-
echo "Alpha: ".$cluster->client('alpha')->get('key').PHP_EOL;
97-
echo "Beta: ".$cluster->client('beta')->get('key').PHP_EOL;
98-
```
99-
100-
### Explicit definition of replicas
101-
102-
The consistent hashing strategy stores keys on a so called "ring". The position of each key is relative to the position of its target node. The target node that has the closest position will be the selected node for that specific key.
103-
104-
To avoid an uneven distribution of keys (especially on small clusters), it is common to duplicate target nodes. Based on the number of replicas, each target node will exist *n times* on the "ring".
105-
106-
The following example explicitly sets the number of replicas to 5. Both Redis instances will have 5 copies. The default value is 128.
107-
108-
```php
109-
<?php
110-
require 'Credis/Client.php';
111-
require 'Credis/Cluster.php';
112-
11390
$cluster = new Credis_Cluster(
114-
array(
115-
array('host' => '127.0.0.1', 'port' => 6379, 'alias'=>'alpha'),
116-
array('host' => '127.0.0.1', 'port' => 6380, 'alias'=>'beta')
117-
), 5
91+
null, // $clusterName // Optional. Name from redis.ini. See https://github.com/phpredis/phpredis/blob/develop/cluster.md
92+
['redis-node-1:6379', 'redis-node-2:6379', 'redis-node-3:6379'], // $clusterSeeds // don't need all nodes, as it pulls that info from one randomly
93+
null, // $timeout
94+
null, // $readTimeout
95+
false, //$persistentBool
96+
'TopSecretPassword', // $password
97+
null, //$username
98+
null //$tlsOptions
11899
);
119100
$cluster->set('key','value');
120-
echo "Alpha: ".$cluster->client('alpha')->get('key').PHP_EOL;
121-
echo "Beta: ".$cluster->client('beta')->get('key').PHP_EOL;
101+
echo "Get: ".$cluster->get('key').PHP_EOL;
122102
```
103+
The Credis_Cluster constructor can either take a cluster name (from redis.ini) or a seed of cluster nodes (An array of strings which can be hostnames or IP address, followed by ports). RedisCluster gets cluster information from one of the seeds at random, so we don't need to pass it all the nodes, and don't need to worry if new nodes are added to cluster.
104+
Many methods of Credis_Cluster are compatible with Credis_Client, but there are some differences.
123105

124-
## Master/slave replication
106+
### Differences between the Credis_Client and Credis_Cluster classes
125107

126-
The [Credis_Cluster](Cluster.php) class can also be used for [master/slave replication](http://redis.io/topics/replication).
127-
Credis_Cluster will automatically perform *read/write splitting* and send the write requests exclusively to the master server.
128-
Read requests will be handled by all servers unless you set the *write_only* flag to true in the connection string of the master server.
129-
130-
### Redis server settings for master/slave replication
131-
132-
Setting up master/slave replication is simple and only requires adding the following line to the config of the slave server:
133-
134-
```
135-
slaveof 127.0.0.1 6379
136-
```
137-
138-
### Basic master/slave example
139-
```php
140-
<?php
141-
require 'Credis/Client.php';
142-
require 'Credis/Cluster.php';
143-
144-
$cluster = new Credis_Cluster(array(
145-
array('host' => '127.0.0.1', 'port' => 6379, 'alias'=>'master', 'master'=>true),
146-
array('host' => '127.0.0.1', 'port' => 6380, 'alias'=>'slave')
147-
));
148-
$cluster->set('key','value');
149-
echo $cluster->get('key').PHP_EOL;
150-
echo $cluster->client('slave')->get('key').PHP_EOL;
151-
152-
$cluster->client('master')->set('key2','value');
153-
echo $cluster->client('slave')->get('key2').PHP_EOL;
154-
```
155-
156-
### No read on master
157-
158-
The following example illustrates how to disable reading on the master server. This will cause the master server only to be used for writing.
159-
This should only happen when you have enough write calls to create a certain load on the master server. Otherwise this is an inefficient usage of server resources.
160-
161-
```php
162-
<?php
163-
require 'Credis/Client.php';
164-
require 'Credis/Cluster.php';
165-
166-
$cluster = new Credis_Cluster(array(
167-
array('host' => '127.0.0.1', 'port' => 6379, 'alias'=>'master', 'master'=>true, 'write_only'=>true),
168-
array('host' => '127.0.0.1', 'port' => 6380, 'alias'=>'slave')
169-
));
170-
$cluster->set('key','value');
171-
echo $cluster->get('key').PHP_EOL;
172-
```
173-
## Automatic failover with Sentinel
174-
175-
[Redis Sentinel](http://redis.io/topics/sentinel) is a system that can monitor Redis instances. You register master servers and Sentinel automatically detects its slaves.
176-
177-
When a master server dies, Sentinel will make sure one of the slaves is promoted to be the new master. This autofailover mechanism will also demote failed masters to avoid data inconsistency.
178-
179-
The [Credis_Sentinel](Sentinel.php) class interacts with the *Redis Sentinel* instance(s) and acts as a proxy. Sentinel will automatically create [Credis_Cluster](Cluster.php) objects and will set the master and slaves accordingly.
180-
181-
Sentinel uses the same protocol as Redis. In the example below we register the Sentinel server running on port *26379* and assign it to the [Credis_Sentinel](Sentinel.php) object.
182-
We then ask Sentinel the hostname and port for the master server known as *mymaster*. By calling the *getCluster* method we immediately get a [Credis_Cluster](Cluster.php) object that allows us to perform basic Redis calls.
183-
184-
```php
185-
<?php
186-
require 'Credis/Client.php';
187-
require 'Credis/Cluster.php';
188-
require 'Credis/Sentinel.php';
189-
190-
$sentinel = new Credis_Sentinel(new Credis_Client('127.0.0.1',26379));
191-
$masterAddress = $sentinel->getMasterAddressByName('mymaster');
192-
$cluster = $sentinel->getCluster('mymaster');
193-
194-
echo 'Writing to master: '.$masterAddress[0].' on port '.$masterAddress[1].PHP_EOL;
195-
$cluster->set('key','value');
196-
echo $cluster->get('key').PHP_EOL;
197-
```
198-
### Additional parameters
199-
200-
Because [Credis_Sentinel](Sentinel.php) will create [Credis_Cluster](Cluster.php) objects using the *"getCluster"* or *"createCluster"* methods, additional parameters can be passed.
201-
202-
First of all there's the *"write_only"* flag. You can also define the selected database and the number of replicas. And finally there's a *"selectRandomSlave"* option.
203-
204-
The *"selectRandomSlave"* flag is used in setups for masters that have multiple slaves. The Credis_Sentinel will either select one random slave to be used when creating the Credis_Cluster object or to pass them all and use the built-in hashing.
205-
206-
The example below shows how to use these 3 options. It selects database 2, sets the number of replicas to 10, it doesn't select a random slave and doesn't allow reading on the master server.
207-
208-
```php
209-
<?php
210-
require 'Credis/Client.php';
211-
require 'Credis/Cluster.php';
212-
require 'Credis/Sentinel.php';
213-
214-
$sentinel = new Credis_Sentinel(new Credis_Client('127.0.0.1',26379));
215-
$cluster = $sentinel->getCluster('mymaster',2,10,false,true);
216-
$cluster->set('key','value');
217-
echo $cluster->get('key').PHP_EOL;
218-
```
108+
* RedisCluster currently has limitations like not supporting pipeline or multi. This may be added in the future. See [here](https://github.com/phpredis/phpredis/blob/develop/cluster.md) for details.
109+
* 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(). 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.
110+
* Redis clusters do not support select(), as they only have a single database.
111+
* RedisCluster currently has buggy/broken behaviour for pSubscribe and script. This appears to be a bug and hopefully will be fixed in the future.
219112

220-
## About
113+
### Note about tlsOptions for Credis_Cluster
114+
Because of weirdness in the behaviour of the $tlsOptions parameter of Credis_Cluster, when a seed is defined with a URL that starts with tls:// or ssl://, if $tlsOptions is null, then it will still try to connect without TLS, and it will fail. This odd behaviour is because the connections to the nodes are gotten from the CLUSTER SLOTS command and those hostnames or IP address do not get prefixed with tls:// or ssl://, and it uses the existance of $tlsOptions array for determining which type of connection to make. If you need TLS connection, the $tlsOptions value MUST be either an empty array, or an array with values. If you want the connections to be made without TLS, then the $tlsOptions array MUST be null.
221115

222116
&copy; 2011 [Colin Mollenhour](http://colin.mollenhour.com)
223117
&copy; 2009 [Justin Poliey](http://justinpoliey.com)

Sentinel.php

Lines changed: 1 addition & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
* Implements the Sentinel API as mentioned on http://redis.io/topics/sentinel.
77
* Sentinel is aware of master and slave nodes in a cluster and returns instances of Credis_Client accordingly.
88
*
9-
* The complexity of read/write splitting can also be abstract by calling the createCluster() method which returns a
10-
* Credis_Cluster object that contains both the master server and a random slave. Credis_Cluster takes care of the
11-
* read/write splitting
12-
*
139
* @author Thijs Feryn <thijs@feryn.eu>
1410
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
1511
* @package Credis_Sentinel
@@ -25,6 +21,7 @@ class Credis_Sentinel
2521

2622
/**
2723
* Contains an active instance of Credis_Cluster per master pool
24+
* @deprecated no longer used
2825
* @var array
2926
*/
3027
protected $_cluster = array();
@@ -250,74 +247,6 @@ public function getSlaveClients($name)
250247
return $this->_slaves[$name];
251248
}
252249

253-
/**
254-
* Returns a Redis cluster object containing a random slave and the master
255-
* When $selectRandomSlave is true, only one random slave is passed.
256-
* When $selectRandomSlave is false, all clients are passed and hashing is applied in Credis_Cluster
257-
* When $writeOnly is false, the master server will also be used for read commands.
258-
* When $masterOnly is true, only the master server will also be used for both read and write commands. $writeOnly will be ignored and forced to set to false.
259-
* @param string $name
260-
* @param int $db
261-
* @param int $replicas
262-
* @param bool $selectRandomSlave
263-
* @param bool $writeOnly
264-
* @param bool $masterOnly
265-
* @return Credis_Cluster
266-
* @throws CredisException
267-
* @deprecated
268-
*/
269-
public function createCluster($name, $db = 0, $replicas = 128, $selectRandomSlave = true, $writeOnly = false, $masterOnly = false)
270-
{
271-
$clients = array();
272-
$workingClients = array();
273-
$master = $this->master($name);
274-
if (strstr($master[9], 's_down') || strstr($master[9], 'disconnected')) {
275-
throw new CredisException('The master is down');
276-
}
277-
if (!$masterOnly) {
278-
$slaves = $this->slaves($name);
279-
foreach ($slaves as $slave) {
280-
if (!strstr($slave[9], 's_down') && !strstr($slave[9], 'disconnected')) {
281-
$workingClients[] = array('host' => $slave[3], 'port' => $slave[5], 'master' => false, 'db' => $db, 'password' => $this->_password);
282-
}
283-
}
284-
if (count($workingClients) > 0) {
285-
if ($selectRandomSlave) {
286-
if (!$writeOnly) {
287-
$workingClients[] = array('host' => $master[3], 'port' => $master[5], 'master' => false, 'db' => $db, 'password' => $this->_password);
288-
}
289-
$clients[] = $workingClients[rand(0, count($workingClients) - 1)];
290-
} else {
291-
$clients = $workingClients;
292-
}
293-
}
294-
} else {
295-
$writeOnly = false;
296-
}
297-
$clients[] = array('host' => $master[3], 'port' => $master[5], 'db' => $db, 'master' => true, 'write_only' => $writeOnly, 'password' => $this->_password);
298-
return new Credis_Cluster($clients, $replicas, $this->_standAlone);
299-
}
300-
301-
/**
302-
* If a Credis_Cluster object exists, return it. Otherwise create one and return it.
303-
* @param string $name
304-
* @param int $db
305-
* @param int $replicas
306-
* @param bool $selectRandomSlave
307-
* @param bool $writeOnly
308-
* @param bool $masterOnly
309-
* @return Credis_Cluster
310-
* @throws CredisException
311-
* @deprecated
312-
*/
313-
public function getCluster($name, $db = 0, $replicas = 128, $selectRandomSlave = true, $writeOnly = false, $masterOnly = false)
314-
{
315-
if (!isset($this->_cluster[$name])) {
316-
$this->_cluster[$name] = $this->createCluster($name, $db, $replicas, $selectRandomSlave, $writeOnly, $masterOnly);
317-
}
318-
return $this->_cluster[$name];
319-
}
320-
321250
/**
322251
* Catch-all method
323252
* @param string $name

phpunit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
<file>tests/CredisTest.php</file>
66
<file>tests/CredisStandaloneTest.php</file>
77
</testsuite>
8+
89
<testsuite name="Cluster">
910
<file>tests/CredisClusterTest.php</file>
10-
<file>tests/CredisStandaloneClusterTest.php</file>
1111
</testsuite>
1212
<testsuite name="Sentinel">
1313
<file>tests/CredisSentinelTest.php</file>

phpunit_local.sh

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
#!/usr/bin/env bash
22

33
# This script runs unit tests locally in environment similar to Travis-CI
4-
# It runs tests in different PHP versions with suitable PHPUnite version.
4+
# It runs tests in different PHP versions with suitable PHPUnit version.
55
#
66
# You can see results of unit tests execution in console.
77
# Also all execution logs are saved to files phpunit_<date-time>.log
88
#
99
# Prerequisites for running unit tests on local machine:
10-
# - docker
11-
# - docker-compose
10+
# - docker (modern version with compose built-in)
1211
#
1312
# You can find definition of all test environments in folder testenv/
1413
# This folder is not automatically synced with .travis.yml
@@ -17,10 +16,10 @@
1716
cd testenv
1817

1918
# build containers and run tests
20-
docker-compose build && docker-compose up
19+
docker compose build && docker compose up php-74 && docker compose up php-83
2120

2221
# save logs to log file
23-
docker-compose logs --no-color --timestamps | sort >"../phpunit_$(date '+%Y%m%d-%H%M%S').log"
22+
docker compose logs --no-color --timestamps | sort >"../phpunit_$(date '+%Y%m%d-%H%M%S').log"
2423

2524
# remove containers
26-
docker-compose rm -f
25+
docker compose rm -f

testenv/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
REDIS_PASSWORD=password-for-testing
2+
REDIS_NODE_1_SEED=redis-node-1:6379

testenv/docker-compose.yml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
version: '2'
22
services:
3-
3+
4+
php-83:
5+
user: 3523:3523
6+
build: env/php-8.3/
7+
volumes:
8+
- ../:/src/:ro
9+
- certs:/certs:ro
10+
env_file: "./.env"
11+
412
php-74:
13+
user: 3523:3523
514
build: env/php-7.4/
615
volumes:
7-
- ../:/src/
16+
- ../:/src/:ro
17+
- certs:/certs:ro
18+
env_file: "./.env"
19+
20+
volumes:
21+
certs: {}

testenv/env/php-7.4/Dockerfile

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ ENV phpunit_version 7.5
33
ENV redis_version 6.0.8
44

55
RUN apt-get update && \
6-
apt-get install -y wget libssl-dev
6+
apt-get install -y wget libssl-dev redis-tools bind9-dnsutils
77

88
RUN wget https://phar.phpunit.de/phpunit-${phpunit_version}.phar && \
99
chmod +x phpunit-${phpunit_version}.phar && \
@@ -14,12 +14,11 @@ RUN yes '' | pecl install -f redis && \
1414
docker-php-ext-enable redis
1515

1616
# install redis server
17-
RUN wget http://download.redis.io/releases/redis-${redis_version}.tar.gz && \
18-
tar -xzf redis-${redis_version}.tar.gz && \
19-
export BUILD_TLS=yes && \
20-
make -s -C redis-${redis_version} -j
17+
RUN wget http://download.redis.io/releases/redis-${redis_version}.tar.gz
18+
RUN tar -xzf redis-${redis_version}.tar.gz
19+
RUN BUILD_TLS=yes make -s -C redis-${redis_version} -j
2120

2221
CMD PATH=$PATH:/usr/local/bin/:/redis-${redis_version}/src/ && \
23-
cp -rp /src /app && \
24-
cd /app && \
22+
cp -rp /src /tmp/app && \
23+
cd /tmp/app && \
2524
phpunit

testenv/env/php-8.3/Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
FROM php:8.3
2+
ENV phpunit_version 9.6
3+
ENV redis_version 6.0.8
4+
5+
RUN apt-get update && \
6+
apt-get install -y wget libssl-dev redis-tools bind9-dnsutils
7+
8+
RUN wget https://phar.phpunit.de/phpunit-${phpunit_version}.phar && \
9+
chmod +x phpunit-${phpunit_version}.phar && \
10+
mv phpunit-${phpunit_version}.phar /usr/local/bin/phpunit
11+
12+
# install php extension
13+
RUN yes '' | pecl install -f redis && \
14+
docker-php-ext-enable redis
15+
16+
# install redis server
17+
RUN wget http://download.redis.io/releases/redis-${redis_version}.tar.gz
18+
RUN tar -xzf redis-${redis_version}.tar.gz
19+
RUN BUILD_TLS=yes make -s -C redis-${redis_version} -j
20+
21+
CMD PATH=$PATH:/usr/local/bin/:/redis-${redis_version}/src/ && \
22+
cp -rp /src /tmp/app && \
23+
cd /tmp/app && \
24+
phpunit

0 commit comments

Comments
 (0)