class Redis_Cache in Redis 7.3
Same name and namespace in other branches
- 7.2 lib/Redis/Cache.php \Redis_Cache
Because those objects will be spawned during boostrap all its configuration must be set in the settings.php file.
You will find the driver specific implementation in the Redis_Cache_* classes as they may differ in how the API handles transaction, pipelining and return values.
Hierarchy
- class \Redis_Cache implements DrupalCacheInterface
Expanded class hierarchy of Redis_Cache
File
- lib/
Redis/ Cache.php, line 11
View source
class Redis_Cache implements DrupalCacheInterface {
/**
* Default lifetime for permanent items.
* Approximatively 1 year.
*/
const LIFETIME_PERM_DEFAULT = 31536000;
/**
* Uses EVAL scripts to flush data when called
*
* This remains the default behavior and is safe until you use a single
* Redis server instance and its version is >= 2.6 (older version don't
* support EVAL).
*/
const FLUSH_NORMAL = 0;
/**
* This mode is tailored for sharded Redis servers instances usage: it
* will never delete entries but only mark the latest flush timestamp
* into one of the servers in the shard. It will proceed to delete on
* read single entries when invalid entries are being loaded.
*/
const FLUSH_SHARD = 3;
/**
* Same as the one above, plus attempt to do pipelining when possible.
*
* This is supposed to work with sharding proxies that supports
* pipelining themselves, such as Twemproxy.
*/
const FLUSH_SHARD_WITH_PIPELINING = 4;
/**
* Computed keys are let's say arround 60 characters length due to
* key prefixing, which makes 1,000 keys DEL command to be something
* arround 50,000 bytes length: this is huge and may not pass into
* Redis, let's split this off.
* Some recommend to never get higher than 1,500 bytes within the same
* command which makes us forced to split this at a very low threshold:
* 20 seems a safe value here (1,280 average length).
*/
const KEY_THRESHOLD = 20;
/**
* @var Redis_Cache_BackendInterface
*/
protected $backend;
/**
* @var string
*/
protected $bin;
/**
* When the global 'cache_lifetime' Drupal variable is set to a value, the
* cache backends should not expire temporary entries by themselves per
* Drupal signature. Volatile items will be dropped accordingly to their
* set lifetime.
*
* @var boolean
*/
protected $allowTemporaryFlush = true;
/**
* When in shard mode, the backend cannot proceed to multiple keys
* operations, and won't delete keys on flush calls.
*
* @var boolean
*/
protected $isSharded = false;
/**
* When in shard mode, the proxy may or may not support pipelining,
* Twemproxy is known to support it.
*
* @var boolean
*/
protected $allowPipeline = false;
/**
* Default TTL for CACHE_PERMANENT items.
*
* See "Default lifetime for permanent items" section of README.txt
* file for a comprehensive explaination of why this exists.
*
* @var int
*/
protected $permTtl = self::LIFETIME_PERM_DEFAULT;
/**
* Maximum TTL for this bin from Drupal configuration.
*
* @var int
*/
protected $maxTtl = 0;
/**
* Flush permanent and volatile cached values
*
* @var string[]
* First value is permanent latest flush time and second value
* is volatile latest flush time
*/
protected $flushCache = null;
/**
* Is this bin in shard mode
*
* @return boolean
*/
public function isSharded() {
return $this->isSharded;
}
/**
* Does this bin allow pipelining through sharded environment
*
* @return boolean
*/
public function allowPipeline() {
return $this->allowPipeline;
}
/**
* Does this bin allow temporary item flush
*
* @return boolean
*/
public function allowTemporaryFlush() {
return $this->allowTemporaryFlush;
}
/**
* Get TTL for CACHE_PERMANENT items.
*
* @return int
* Lifetime in seconds.
*/
public function getPermTtl() {
return $this->permTtl;
}
/**
* Get maximum TTL for all items.
*
* @return int
* Lifetime in seconds.
*/
public function getMaxTtl() {
return $this->maxTtl;
}
/**
* {@inheritdoc}
*/
public function __construct($bin) {
$this->bin = $bin;
$className = Redis_Client::getClass(Redis_Client::REDIS_IMPL_CACHE);
$this->backend = new $className(Redis_Client::getClient(), $bin, Redis_Client::getDefaultPrefix($bin));
$this
->refreshCapabilities();
$this
->refreshPermTtl();
$this
->refreshMaxTtl();
}
/**
* Find from Drupal variables the clear mode.
*/
public function refreshCapabilities() {
if (0 < variable_get('cache_lifetime', 0)) {
// Per Drupal default behavior, when the 'cache_lifetime' variable
// is set we must not flush any temporary items since they have a
// life time.
$this->allowTemporaryFlush = false;
}
if (null !== ($mode = variable_get('redis_flush_mode', null))) {
$mode = (int) $mode;
}
else {
$mode = self::FLUSH_NORMAL;
}
$this->isSharded = self::FLUSH_SHARD === $mode || self::FLUSH_SHARD_WITH_PIPELINING === $mode;
$this->allowPipeline = self::FLUSH_SHARD !== $mode;
}
/**
* Find from Drupal variables the right permanent items TTL.
*/
protected function refreshPermTtl() {
$ttl = null;
if (null === ($ttl = variable_get('redis_perm_ttl_' . $this->bin, null))) {
if (null === ($ttl = variable_get('redis_perm_ttl', null))) {
$ttl = self::LIFETIME_PERM_DEFAULT;
}
}
if ($ttl === (int) $ttl) {
$this->permTtl = $ttl;
}
else {
if ($iv = DateInterval::createFromDateString($ttl)) {
// http://stackoverflow.com/questions/14277611/convert-dateinterval-object-to-seconds-in-php
$this->permTtl = $iv->y * 31536000 + $iv->m * 2592000 + $iv->d * 86400 + $iv->h * 3600 + $iv->i * 60 + $iv->s;
}
else {
// Sorry but we have to log this somehow.
trigger_error(sprintf("Parsed TTL '%s' has an invalid value: switching to default", $ttl));
$this->permTtl = self::LIFETIME_PERM_DEFAULT;
}
}
}
/**
* Find from Drupal variables the maximum cache lifetime.
*/
public function refreshMaxTtl() {
// And now cache lifetime. Be aware we exclude negative values
// considering those are Drupal misconfiguration.
$maxTtl = variable_get('cache_lifetime', 0);
if (0 < $maxTtl) {
if ($maxTtl < $this->permTtl) {
$this->maxTtl = $maxTtl;
}
else {
$this->maxTtl = $this->permTtl;
}
}
else {
if ($this->permTtl) {
$this->maxTtl = $this->permTtl;
}
}
}
/**
* Set last flush time
*
* @param string $permanent
* @param string $volatile
*/
public function setLastFlushTime($permanent = false, $volatile = false) {
// Here we need to fetch absolute values from backend, to avoid
// concurrency problems and ensure data validity.
list($flushPerm, $flushVolatile) = $this->backend
->getLastFlushTime();
$checksum = $this
->getValidChecksum(max(array(
$flushPerm,
$flushVolatile,
$permanent,
time(),
)));
if ($permanent) {
$this->backend
->setLastFlushTimeFor($checksum, false);
$this->backend
->setLastFlushTimeFor($checksum, true);
$this->flushCache = array(
$checksum,
$checksum,
);
}
else {
if ($volatile) {
$this->backend
->setLastFlushTimeFor($checksum, true);
$this->flushCache = array(
$flushPerm,
$checksum,
);
}
}
}
/**
* Get latest flush time
*
* @return string[]
* First value is the latest flush time for permanent entries checksum,
* second value is the latest flush time for volatile entries checksum.
*/
public function getLastFlushTime() {
if (!$this->flushCache) {
$this->flushCache = $this->backend
->getLastFlushTime();
}
// At the very first hit, we might not have the timestamps set, thus
// we need to create them to avoid our entry being considered as
// invalid
if (!$this->flushCache[0]) {
$this
->setLastFlushTime(true, true);
}
else {
if (!$this->flushCache[1]) {
$this
->setLastFlushTime(false, true);
}
}
return $this->flushCache;
}
/**
* Create cache entry
*
* @param string $cid
* @param mixed $data
*
* @return array
*/
protected function createEntryHash($cid, $data, $expire = CACHE_PERMANENT) {
list($flushPerm, $flushVolatile) = $this
->getLastFlushTime();
if (CACHE_TEMPORARY === $expire) {
$validityThreshold = max(array(
$flushVolatile,
$flushPerm,
));
}
else {
$validityThreshold = $flushPerm;
}
$time = $this
->getValidChecksum($validityThreshold);
$hash = array(
'cid' => $cid,
'created' => $time,
'expire' => $expire,
);
// Let Redis handle the data types itself.
if (!is_string($data)) {
$hash['data'] = serialize($data);
$hash['serialized'] = 1;
}
else {
$hash['data'] = $data;
$hash['serialized'] = 0;
}
return $hash;
}
/**
* Expand cache entry from fetched data
*
* @param array $values
* Raw values fetched from Redis server data
*
* @return array
* Or FALSE if entry is invalid
*/
protected function expandEntry(array $values, $flushPerm, $flushVolatile) {
// Check for entry being valid.
if (empty($values['cid'])) {
return;
}
// This ensures backward compatibility with older version of
// this module's data still stored in Redis.
if (isset($values['expire'])) {
$expire = (int) $values['expire'];
// Ensure the entry is valid and have not expired.
if ($expire !== CACHE_PERMANENT && $expire !== CACHE_TEMPORARY && $expire <= time()) {
return false;
}
}
// Ensure the entry does not predate the last flush time.
if ($this->allowTemporaryFlush && !empty($values['volatile'])) {
$validityThreshold = max(array(
$flushPerm,
$flushVolatile,
));
}
else {
$validityThreshold = $flushPerm;
}
if ($values['created'] <= $validityThreshold) {
return false;
}
$entry = (object) $values;
// Reduce the checksum to the real timestamp part
$entry->created = (int) $entry->created;
if ($entry->serialized) {
$entry->data = unserialize($entry->data);
}
return $entry;
}
/**
* {@inheritdoc}
*/
public function get($cid) {
$values = $this->backend
->get($cid);
if (empty($values)) {
return false;
}
list($flushPerm, $flushVolatile) = $this
->getLastFlushTime();
$entry = $this
->expandEntry($values, $flushPerm, $flushVolatile);
if (!$entry) {
// This entry exists but is invalid.
$this->backend
->delete($cid);
return false;
}
return $entry;
}
/**
* {@inheritdoc}
*/
public function getMultiple(&$cids) {
$ret = array();
$delete = array();
if (!$this->allowPipeline) {
$entries = array();
foreach ($cids as $cid) {
if ($entry = $this->backend
->get($cid)) {
$entries[$cid] = $entry;
}
}
}
else {
$entries = $this->backend
->getMultiple($cids);
}
list($flushPerm, $flushVolatile) = $this
->getLastFlushTime();
foreach ($cids as $key => $cid) {
if (!empty($entries[$cid])) {
$entry = $this
->expandEntry($entries[$cid], $flushPerm, $flushVolatile);
}
else {
$entry = null;
}
if (empty($entry)) {
$delete[] = $cid;
}
else {
$ret[$cid] = $entry;
unset($cids[$key]);
}
}
if (!empty($delete)) {
if ($this->allowPipeline) {
foreach ($delete as $id) {
$this->backend
->delete($id);
}
}
else {
$this->backend
->deleteMultiple($delete);
}
}
return $ret;
}
/**
* {@inheritdoc}
*/
public function set($cid, $data, $expire = CACHE_PERMANENT) {
$hash = $this
->createEntryHash($cid, $data, $expire);
$maxTtl = $this
->getMaxTtl();
$permTtl = $this
->getPermTtl();
switch ($expire) {
case CACHE_PERMANENT:
$this->backend
->set($cid, $hash, $permTtl, false);
break;
case CACHE_TEMPORARY:
$this->backend
->set($cid, $hash, $maxTtl, true);
break;
default:
$ttl = $expire - time();
// Ensure $expire consistency
if ($ttl <= 0) {
// Entry has already expired, but we may have a stalled
// older cache entry remaining there, ensure it wont
// happen by doing a preventive delete
$this->backend
->delete($cid);
}
else {
if ($maxTtl && $maxTtl < $ttl) {
$ttl = $maxTtl;
}
$this->backend
->set($cid, $hash, $ttl, false);
}
break;
}
}
/**
* {@inheritdoc}
*/
public function clear($cid = null, $wildcard = false) {
if (null === $cid && !$wildcard) {
// Drupal asked for volatile entries flush, this will happen
// during cron run, mostly
$this
->setLastFlushTime(false, true);
if (!$this->isSharded && $this->allowTemporaryFlush) {
$this->backend
->flushVolatile();
}
}
else {
if ($wildcard) {
if (empty($cid)) {
// This seems to be an error, just do nothing.
return;
}
if ('*' === $cid) {
// Use max() to ensure we invalidate both correctly
$this
->setLastFlushTime(true);
if (!$this->isSharded) {
$this->backend
->flush();
}
}
else {
if (!$this->isSharded) {
$this->backend
->deleteByPrefix($cid);
}
else {
// @todo This needs a map algorithm the same way memcache
// module implemented it for invalidity by prefixes. This
// is a very stupid fallback
$this
->setLastFlushTime(true);
}
}
}
else {
if (is_array($cid)) {
$this->backend
->deleteMultiple($cid);
}
else {
$this->backend
->delete($cid);
}
}
}
}
public function isEmpty() {
return false;
}
/**
* From the given timestamp build an incremental safe time-based identifier.
*
* Due to potential accidental cache wipes, when a server goes down in the
* cluster or when a server triggers its LRU algorithm wipe-out, keys that
* matches flush or tags checksum might be dropped.
*
* Per default, each new inserted tag will trigger a checksum computation to
* be stored in the Redis server as a timestamp. In order to ensure a checksum
* validity a simple comparison between the tag checksum and the cache entry
* checksum will tell us if the entry pre-dates the current checksum or not,
* thus telling us its state. The main problem we experience is that Redis
* is being so fast it is able to create and drop entries at same second,
* sometime even the same micro second. The only safe way to avoid conflicts
* is to checksum using an arbitrary computed number (a sequence).
*
* Drupal core does exactly this thus tags checksums are additions of each tag
* individual checksum; each tag checksum is a independent arbitrary serial
* that gets incremented starting with 0 (no invalidation done yet) to n (n
* invalidations) which grows over time. This way the checksum computation
* always rises and we have a sensible default that works in all cases.
*
* This model works as long as you can ensure consistency for the serial
* storage over time. Nevertheless, as explained upper, in our case this
* serial might be dropped at some point for various valid technical reasons:
* if we start over to 0, we may accidentally compute a checksum which already
* existed in the past and make invalid entries turn back to valid again.
*
* In order to prevent this behavior, using a timestamp as part of the serial
* ensures that we won't experience this problem in a time range wider than a
* single second, which is safe enough for us. But using timestamp creates a
* new problem: Redis is so fast that we can set or delete hundreds of entries
* easily during the same second: an entry created then invalidated the same
* second will create false positives (entry is being considered as valid) -
* note that depending on the check algorithm, false negative may also happen
* the same way. Therefore we need to have an abitrary serial value to be
* incremented in order to enforce our checks to be more strict.
*
* The solution to both the first (the need for a time based checksum in case
* of checksum data being dropped) and the second (the need to have an
* arbitrary predictible serial value to avoid false positives or negatives)
* we are combining the two: every checksum will be built this way:
*
* UNIXTIMESTAMP.SERIAL
*
* For example:
*
* 1429789217.017
*
* will reprensent the 17th invalidation of the 1429789217 exact second which
* happened while writing this documentation. The next tag being invalidated
* the same second will then have this checksum:
*
* 1429789217.018
*
* And so on...
*
* In order to make it consitent with PHP string and float comparison we need
* to set fixed precision over the decimal, and store as a string to avoid
* possible float precision problems when comparing.
*
* This algorithm is not fully failsafe, but allows us to proceed to 1000
* operations on the same checksum during the same second, which is a
* sufficiently great value to reduce the conflict probability to almost
* zero for most uses cases.
*
* @param int|string $timestamp
* "TIMESTAMP[.INCREMENT]" string
*
* @return string
* The next "TIMESTAMP.INCREMENT" string.
*/
public function getNextIncrement($timestamp = null) {
if (!$timestamp) {
return time() . '.000';
}
if (false !== ($pos = strpos($timestamp, '.'))) {
$inc = substr($timestamp, $pos + 1, 3);
return (int) $timestamp . '.' . str_pad($inc + 1, 3, '0', STR_PAD_LEFT);
}
return $timestamp . '.000';
}
/**
* Get valid checksum
*
* @param int|string $previous
* "TIMESTAMP[.INCREMENT]" string
*
* @return string
* The next "TIMESTAMP.INCREMENT" string.
*
* @see Redis_Cache::getNextIncrement()
*/
public function getValidChecksum($previous = null) {
if (time() === (int) $previous) {
return $this
->getNextIncrement($previous);
}
else {
return $this
->getNextIncrement();
}
}
}
Members
Name![]() |
Modifiers | Type | Description | Overrides |
---|---|---|---|---|
Redis_Cache:: |
protected | property | When in shard mode, the proxy may or may not support pipelining, Twemproxy is known to support it. | |
Redis_Cache:: |
protected | property | When the global 'cache_lifetime' Drupal variable is set to a value, the cache backends should not expire temporary entries by themselves per Drupal signature. Volatile items will be dropped accordingly to their set lifetime. | |
Redis_Cache:: |
protected | property | ||
Redis_Cache:: |
protected | property | ||
Redis_Cache:: |
protected | property | Flush permanent and volatile cached values | |
Redis_Cache:: |
protected | property | When in shard mode, the backend cannot proceed to multiple keys operations, and won't delete keys on flush calls. | |
Redis_Cache:: |
protected | property | Maximum TTL for this bin from Drupal configuration. | |
Redis_Cache:: |
protected | property | Default TTL for CACHE_PERMANENT items. | |
Redis_Cache:: |
public | function | Does this bin allow pipelining through sharded environment | |
Redis_Cache:: |
public | function | Does this bin allow temporary item flush | |
Redis_Cache:: |
public | function |
Expires data from the cache. Overrides DrupalCacheInterface:: |
|
Redis_Cache:: |
protected | function | Create cache entry | 1 |
Redis_Cache:: |
protected | function | Expand cache entry from fetched data | 1 |
Redis_Cache:: |
constant | Uses EVAL scripts to flush data when called | ||
Redis_Cache:: |
constant | This mode is tailored for sharded Redis servers instances usage: it will never delete entries but only mark the latest flush timestamp into one of the servers in the shard. It will proceed to delete on read single entries when invalid entries are… | ||
Redis_Cache:: |
constant | Same as the one above, plus attempt to do pipelining when possible. | ||
Redis_Cache:: |
public | function |
Returns data from the persistent cache. Overrides DrupalCacheInterface:: |
|
Redis_Cache:: |
public | function | Get latest flush time | |
Redis_Cache:: |
public | function | Get maximum TTL for all items. | |
Redis_Cache:: |
public | function |
Returns data from the persistent cache when given an array of cache IDs. Overrides DrupalCacheInterface:: |
|
Redis_Cache:: |
public | function | From the given timestamp build an incremental safe time-based identifier. | |
Redis_Cache:: |
public | function | Get TTL for CACHE_PERMANENT items. | |
Redis_Cache:: |
public | function | Get valid checksum | |
Redis_Cache:: |
public | function |
Checks if a cache bin is empty. Overrides DrupalCacheInterface:: |
|
Redis_Cache:: |
public | function | Is this bin in shard mode | |
Redis_Cache:: |
constant | Computed keys are let's say arround 60 characters length due to key prefixing, which makes 1,000 keys DEL command to be something arround 50,000 bytes length: this is huge and may not pass into Redis, let's split this off. Some recommend to… | ||
Redis_Cache:: |
constant | Default lifetime for permanent items. Approximatively 1 year. | ||
Redis_Cache:: |
public | function | Find from Drupal variables the clear mode. | |
Redis_Cache:: |
public | function | Find from Drupal variables the maximum cache lifetime. | |
Redis_Cache:: |
protected | function | Find from Drupal variables the right permanent items TTL. | |
Redis_Cache:: |
public | function |
Stores data in the persistent cache. Overrides DrupalCacheInterface:: |
|
Redis_Cache:: |
public | function | Set last flush time | |
Redis_Cache:: |
public | function | 1 |