Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#66 distribute the read operation on Redis Cache #132

Merged
merged 9 commits into from
Jan 4, 2018
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Yii Framework 2 redis extension Change Log
2.0.8 under development
-----------------------

- no changes in this release.
- Enh #66: Cache component can be configured to read / get from replicas (ryusoft)


2.0.7 December 11, 2017
Expand Down
84 changes: 82 additions & 2 deletions Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@
* ]
* ~~~
*
* If you have multiple redis replicas (e.g. AWS ElasticCache Redis) you can configure the cache to
* send read operations to the replicas. If no replicas are configured, all operations will be performed on the
* master connection configured via the [[redis]] property.
*
* ~~~
* [
* 'components' => [
* 'cache' => [
* 'class' => 'yii\redis\Cache',
* 'enableReplicas' => true,
* 'replicas' => [
* // config for replica redis connections, (default class will be yii\redis\Connection if not provided)
* // you can optionally put in master as hostname as well, as all GET operation will use replicas
* ['hostname' => 'redis-master.xyz.ng.0001.apse1.cache.amazonaws.com'],
* ['hostname' => 'redis-slave-002.xyz.0001.apse1.cache.amazonaws.com'],
* ['hostname' => 'redis-slave-003.xyz.0001.apse1.cache.amazonaws.com'],
* ],
* ],
* ],
* ]
* ~~~
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
Expand All @@ -66,6 +88,36 @@ class Cache extends \yii\caching\Cache
* with a Redis [[Connection]] object.
*/
public $redis = 'redis';
/**
* @var bool whether to enable read / get from redis replicas.
* @since 2.0.8
* @see $replicas
*/
public $enableReplicas = false;
/**
* @var array the Redis [[Connection]] configurations for redis replicas.
* Each entry is a class configuration, which will be used to instantiate a replica connection.
* The default class is [[Connection|yii\redis\Connection]]. You should at least provide a hostname.
*
* Configuration example:
*
* ```php
* 'replicas' => [
* ['hostname' => 'redis-master.xyz.ng.0001.apse1.cache.amazonaws.com'],
* ['hostname' => 'redis-slave-002.xyz.0001.apse1.cache.amazonaws.com'],
* ['hostname' => 'redis-slave-003.xyz.0001.apse1.cache.amazonaws.com'],
* ],
* ```
*
* @since 2.0.8
* @see $enableReplicas
*/
public $replicas = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @since tag and description.


/**
* @var Connection currently active connection.
*/
private $_replica;


/**
Expand Down Expand Up @@ -99,15 +151,15 @@ public function exists($key)
*/
protected function getValue($key)
{
return $this->redis->executeCommand('GET', [$key]);
return $this->getReplica()->executeCommand('GET', [$key]);
}

/**
* @inheritdoc
*/
protected function getValues($keys)
{
$response = $this->redis->executeCommand('MGET', $keys);
$response = $this->getReplica()->executeCommand('MGET', $keys);
$result = [];
$i = 0;
foreach ($keys as $key) {
Expand Down Expand Up @@ -195,4 +247,32 @@ protected function flushValues()
{
return $this->redis->executeCommand('FLUSHDB');
}

/**
* It will return the current Replica Redis [[Connection]], and fall back to default [[redis]] [[Connection]]
* defined in this instance. Only used in getValue() and getValues().
* @since 2.0.8
* @return array|string|Connection
* @throws \yii\base\InvalidConfigException
*/
protected function getReplica()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a new method. Thus, @inheritdoc doesn't make sense. Missing @since tag and description.

{
if ($this->enableReplicas === false) {
return $this->redis;
}

if ($this->_replica !== null) {
return $this->_replica;
}

if (empty($this->replicas)) {
return $this->_replica = $this->redis;
}

$replicas = $this->replicas;
shuffle($replicas);
$config = array_shift($replicas);
$this->_replica = Instance::ensure($config, Connection::className());
return $this->_replica;
}
}
75 changes: 75 additions & 0 deletions tests/RedisCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ protected function getCacheInstance()
return $this->_cacheInstance;
}

protected function resetCacheInstance()
{
$this->getCacheInstance()->flush();
$this->_cacheInstance = null;
}

public function testExpireMilliseconds()
{
$cache = $this->getCacheInstance();
Expand Down Expand Up @@ -120,4 +126,73 @@ public function testMultiByteGetAndSet()
$cache->set($key, $data);
$this->assertSame($cache->get($key), $data);
}

public function testReplica()
{
$this->resetCacheInstance();

$cache = $this->getCacheInstance();
$cache->enableReplicas = true;

$key = 'replica-1';
$value = 'replica';

//No Replica listed
$this->assertFalse($cache->get($key));
$cache->set($key, $value);
$this->assertSame($cache->get($key), $value);

$cache->replicas = [
['hostname' => 'localhost'],
];
$this->assertSame($cache->get($key), $value);

//One Replica listed
$this->resetCacheInstance();
$cache = $this->getCacheInstance();
$cache->enableReplicas = true;
$cache->replicas = [
['hostname' => 'localhost'],
];
$this->assertFalse($cache->get($key));
$cache->set($key, $value);
$this->assertSame($cache->get($key), $value);

//Multiple Replicas listed
$this->resetCacheInstance();
$cache = $this->getCacheInstance();
$cache->enableReplicas = true;

$cache->replicas = [
['hostname' => '127.0.0.1'],
['hostname' => '127.0.0.1'],
];
$this->assertFalse($cache->get($key));
$cache->set($key, $value);
$this->assertSame($cache->get($key), $value);

//invalid config
$this->resetCacheInstance();
$cache = $this->getCacheInstance();
$cache->enableReplicas = true;

$cache->replicas = [
['class' => 'yii\db\Connection'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should expect an exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial intention of not using Instance::ensure() is not to break the application if replica is not properly configured, will update the test to expect exception

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exception expect to be handled by UnitTest in Instance Class on Instance::ensure() function, therefore I removed the test for invalid config.

];
$this->assertFalse($cache->get($key));
$cache->set($key, $value);
$this->assertSame($cache->get($key), $value);

//invalid config
$this->resetCacheInstance();
$cache = $this->getCacheInstance();
$cache->enableReplicas = true;

$cache->replicas = ['redis'];
$this->assertFalse($cache->get($key));
$cache->set($key, $value);
$this->assertSame($cache->get($key), $value);

$this->resetCacheInstance();
}
}