From eac48ff2a8e9c85f8e17c5cbfe97f1f77d9b0993 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 1 May 2013 16:12:18 +0200 Subject: [PATCH] Redis cache implementation --- framework/caching/MemCache.php | 4 +- framework/caching/RedisCache.php | 191 ++++++++++++++++++++++++ framework/db/redis/Connection.php | 13 +- tests/unit/framework/caching/MemCachedTest.php | 2 +- tests/unit/framework/caching/RedisCacheTest.php | 34 +++++ 5 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 framework/caching/RedisCache.php create mode 100644 tests/unit/framework/caching/RedisCacheTest.php diff --git a/framework/caching/MemCache.php b/framework/caching/MemCache.php index 20aff21..efa89f5 100644 --- a/framework/caching/MemCache.php +++ b/framework/caching/MemCache.php @@ -21,7 +21,7 @@ use yii\base\InvalidConfigException; * MemCache can be configured with a list of memcache servers by settings its [[servers]] property. * By default, MemCache assumes there is a memcache server running on localhost at port 11211. * - * See [[Cache]] for common cache operations that ApcCache supports. + * See [[Cache]] for common cache operations that MemCache supports. * * Note, there is no security measure to protected data in memcache. * All data in memcache can be accessed by any process running in the system. @@ -89,7 +89,7 @@ class MemCache extends Cache if (count($servers)) { foreach ($servers as $server) { if ($server->host === null) { - throw new Exception("The 'host' property must be specified for every memcache server."); + throw new InvalidConfigException("The 'host' property must be specified for every memcache server."); } if ($this->useMemcached) { $cache->addServer($server->host, $server->port, $server->weight); diff --git a/framework/caching/RedisCache.php b/framework/caching/RedisCache.php new file mode 100644 index 0000000..3e45fe0 --- /dev/null +++ b/framework/caching/RedisCache.php @@ -0,0 +1,191 @@ +array( + * 'cache'=>array( + * 'class'=>'RedisCache', + * 'hostname'=>'localhost', + * 'port'=>6379, + * 'database'=>0, + * ), + * ), + * ) + * ~~~ + * + * In the above, two memcache servers are used: server1 and server2. You can configure more properties of + * each server, such as `persistent`, `weight`, `timeout`. Please see [[MemCacheServer]] for available options. + * + * @property \Memcache|\Memcached $memCache The memcache instance (or memcached if [[useMemcached]] is true) used by this component. + * @property MemCacheServer[] $servers List of memcache server configurations. + * + * @author Carsten Brandt + * @since 2.0 + */ +class RedisCache extends Cache +{ + /** + * @var string hostname to use for connecting to the redis server. Defaults to 'localhost'. + */ + public $hostname = 'localhost'; + /** + * @var int the to use for connecting to the redis server. Default port is 6379. + */ + public $port = 6379; + /** + * @var string the password to use to identify with the redis server. If not set, no AUTH command will be sent. + */ + public $password; + /** + * @var int the redis database to use. This is an integer value starting from 0. Defaults to 0. + */ + public $database = 0; + /** + * @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout") + */ + public $timeout = null; + /** + * @var \yii\db\redis\Connection the redis connection + */ + private $_connection; + + + /** + * Initializes the cache component by establishing a connection to the redis server. + */ + public function init() + { + parent::init(); + $this->getConnection(); + } + + /** + * Returns the redis connection object. + * Establishes a connection to the redis server if it does not already exists. + * + * TODO throw exception on error + * @return \yii\db\redis\Connection + */ + public function getConnection() + { + if ($this->_connection === null) { + $this->_connection = new Connection(array( + 'dsn' => 'redis://' . $this->hostname . ':' . $this->port . '/' . $this->database, + 'password' => $this->password, + 'timeout' => $this->timeout, + )); + } + return $this->_connection; + } + + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string $key a unique key identifying the cached value + * @return string the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + return $this->_connection->executeCommand('GET', $key); + } + + /** + * Retrieves multiple values from cache with the specified keys. + * @param array $keys a list of keys identifying the cached values + * @return array a list of cached values indexed by the keys + */ + protected function getValues($keys) + { + return $this->_connection->executeCommand('MGET', $keys); + } + + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param float $expire the number of seconds in which the cached value will expire. 0 means never expire. + * This can be a floating point number to specify the time in milliseconds. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key,$value,$expire) + { + if ($expire == 0) { + return (bool) $this->_connection->executeCommand('SET', array($key, $value)); + } else { + $expire = (int) ($expire * 1000); + return (bool) $this->_connection->executeCommand('PSETEX', array($key, $expire, $value)); + } + } + + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param float $expire the number of seconds in which the cached value will expire. 0 means never expire. + * This can be a floating point number to specify the time in milliseconds. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key,$value,$expire) + { + if ($expire == 0) { + return (bool) $this->_connection->executeCommand('SETNX', array($key, $value)); + } else { + // TODO consider requiring redis version >= 2.6.12 that supports this in one command + $expire = (int) ($expire * 1000); + $this->_connection->executeCommand('MULTI'); + $this->_connection->executeCommand('SETNX', array($key, $value)); + $this->_connection->executeCommand('PEXPIRE', array($key, $expire)); + $response = $this->_connection->executeCommand('EXEC'); + return (bool) $response[0]; + } + } + + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + return (bool) $this->_connection->executeCommand('DEL', array($key)); + } + + /** + * Deletes all values from cache. + * This is the implementation of the method declared in the parent class. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + return $this->_connection->executeCommand('FLUSHDB'); + } +} diff --git a/framework/db/redis/Connection.php b/framework/db/redis/Connection.php index 0ea2c52..ea73fce 100644 --- a/framework/db/redis/Connection.php +++ b/framework/db/redis/Connection.php @@ -45,6 +45,10 @@ class Connection extends Component * See http://redis.io/commands/auth */ public $password; + /** + * @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout") + */ + public $timeout = null; /** * @var array List of available redis commands http://redis.io/commands @@ -237,7 +241,12 @@ class Connection extends Component $db = isset($dsn[3]) ? $dsn[3] : 0; \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); - $this->_socket = @stream_socket_client($host, $errorNumber, $errorDescription); + $this->_socket = @stream_socket_client( + $host, + $errorNumber, + $errorDescription, + $this->timeout ? $this->timeout : ini_get("default_socket_timeout") + ); if ($this->_socket) { if ($this->password !== null) { $this->executeCommand('AUTH', array($this->password)); @@ -337,6 +346,8 @@ class Connection extends Component * @param $name * @param $params * @return array|bool|null|string + * Returns true on Status reply + * TODO explain all reply types */ public function executeCommand($name, $params=array()) { diff --git a/tests/unit/framework/caching/MemCachedTest.php b/tests/unit/framework/caching/MemCachedTest.php index 59396df..dd2eda8 100644 --- a/tests/unit/framework/caching/MemCachedTest.php +++ b/tests/unit/framework/caching/MemCachedTest.php @@ -4,7 +4,7 @@ use yii\caching\MemCache; use yiiunit\TestCase; /** - * Class for testing memcache cache backend + * Class for testing memcached cache backend */ class MemCachedTest extends CacheTest { diff --git a/tests/unit/framework/caching/RedisCacheTest.php b/tests/unit/framework/caching/RedisCacheTest.php new file mode 100644 index 0000000..cc6c304 --- /dev/null +++ b/tests/unit/framework/caching/RedisCacheTest.php @@ -0,0 +1,34 @@ + 'localhost', + 'port' => 6379, + 'database' => 0, + ); + $dsn = $config['hostname'] . ':' .$config['port']; + if(!@stream_socket_client($dsn, $errorNumber, $errorDescription, 0.5)) { + $this->markTestSkipped('No redis server running at ' . $dsn .' : ' . $errorNumber . ' - ' . $errorDescription); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new RedisCache($config); + } + return $this->_cacheInstance; + } +} \ No newline at end of file