Skip to content
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
* 'redis',//id of Redis [[Connection]] Component
* ['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' => [
* 'redis',
* ['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 = [];

/**
* @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()
{
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;
}
}
64 changes: 64 additions & 0 deletions tests/RedisCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace yiiunit\extensions\redis;

use yii\base\InvalidConfigException;
use yii\redis\Cache;
use yii\redis\Connection;
use yiiunit\framework\caching\CacheTestCase;
Expand Down Expand Up @@ -39,6 +40,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 +127,61 @@ 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' => 'localhost'],
['hostname' => 'localhost'],
];
$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();
}
}