Last active
December 11, 2015 05:48
-
-
Save m8rge/4554478 to your computer and use it in GitHub Desktop.
Yii component EMemCache prevents dogpile effect. WARNING! EMemCache internal cache value format incompatible with CMemCache!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
* Код, написанный ниже, подвержен dogpile effect (http://habrahabr.ru/post/43540/) | |
*/ | |
if (false === $result = Yii::app()->cache->get($cacheKey)) { | |
$result = file_get_contents('http://slow.service.ru'); | |
Yii::app()->cache->set($cacheKey, $result); | |
} | |
/* | |
* Используя класс EMemCache, можно избежать dogpile effect использую функцию setWithLocking. | |
* Если 10 потоков пришли на страницу и запросили отсутствующий кеш, то перегенерацию будет выполнять | |
* только один поток, остальные девять ждут (максимально $waitTimeout секунд). Если кеш появился за это | |
* время, то показывают его. Иначе - false и печалька. | |
* Если 10 потоков пришли на страницу с протухшим кешем то перегенерацию будет выполнять только один | |
* поток, остальные девять получат протухший кеш. | |
* | |
* Также, есть проверка следующего случая, когда после блокировки ключа кеша на запись скрипт свалился | |
* и не разблокировал ключ. | |
* Здесь в дело вступает $lockTimeout, по умолчанию равный 60 секундам. | |
*/ | |
$result = Yii::app()->cache->setWithLocking($cacheKey, function($cacheKey) { | |
$result = file_get_contents('http://slow.service.ru'); | |
Yii::app()->cache->set($cacheKey, $result); | |
return $result; | |
}[, $waitTimeout[, $lockTimeout]]); | |
/* | |
* Первым параметров принимается ключ в кеше, который будет изменяться. | |
* Вторым - callable. Единственным параметром в callable передается ключ для изменения. | |
* Внутри функции следует получить значение для кеширования, а затем записать его в кеш. | |
* Также обязательно требуется вернуть полученное значение. | |
* | |
* Функция setWithLocking возвращает результат, который мы вернули из callable. | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Presents lockingForUpdate ability to prevent dogpile effect | |
*/ | |
class EMemCache extends CMemCache | |
{ | |
/** | |
* @param $key string | |
* @param $timeoutSeconds int | |
* @return bool true if unlocked | |
*/ | |
public function waitForUnlock($key, $timeoutSeconds = 5) | |
{ | |
$i = 0; | |
while ($this->get($this->getLockKey($key)) && $i < $timeoutSeconds * 1000000) { | |
usleep(100000); // 100ms | |
$i += 100000; | |
} | |
return $this->get($this->getLockKey($key)) === false; | |
} | |
/** | |
* @param $key string | |
* @param int $timeoutSeconds | |
* @return bool true if successfully locked, false if already locked | |
*/ | |
public function lockForUpdate($key, $timeoutSeconds = 60) | |
{ | |
return $this->add($this->getLockKey($key), true, $timeoutSeconds); | |
} | |
/** | |
* @param $key string | |
* @return bool true if successfully unlocked | |
*/ | |
public function unlock($key) | |
{ | |
return $this->delete($this->getLockKey($key)); | |
} | |
/** | |
* @param $key string | |
* @return string | |
*/ | |
protected function getLockKey($key) | |
{ | |
return $key . '_updateLock'; | |
} | |
/** | |
* if $key value expired - try lock and refresh cache. | |
* If $key already locked, try return expired cache, | |
* if failed - wait $waitTimeout for filling cache, | |
* if failed - return false | |
* @param $key string | |
* @param $updateStatement callable Statement receives as first parameter cache $key to be setted. | |
* Statement must return value, assigned to $key. | |
* Statement executed when no other process locked same key with setWithLocking function. | |
* @param int $waitTimeout Wait for actual value appears in cache, in seconds. | |
* Must be reasonable, because page loading is locked for this time. | |
* @param int $lockTimeout Maximum possible time for waiting cache updating, in seconds. | |
* Used, if $updateStatement breaks down current script. | |
* @throws InvalidArgumentException | |
* @return bool|mixed Actual value in cache | |
*/ | |
public function setWithLocking($key, $updateStatement, $waitTimeout = 5, $lockTimeout = 60) | |
{ | |
if (!is_callable($updateStatement)) { | |
throw new InvalidArgumentException('second arguments is not valid callable'); | |
} | |
if ($this->lockForUpdate($key, $lockTimeout)) { | |
$value = $updateStatement($key); | |
$this->unlock($key); | |
} else { | |
$value = $this->forceGet($key); | |
if (false === $value && $this->waitForUnlock($key, $waitTimeout)) { | |
$value = $this->get($key); | |
} | |
} | |
return $value; | |
} | |
/** | |
* Retrieves a value from cache with a specified key. | |
* @param string $id a key identifying the cached value | |
* @return mixed the value stored in cache, false if the value is not in the cache, expired or the dependency has changed. | |
*/ | |
public function get($id) | |
{ | |
$value = parent::get($id); | |
if (is_array($value) && is_numeric($value[1]) && ($value[1] !== 0 && $value[1] > time() || $value[1] === 0)) { | |
return $value[0]; | |
} else { | |
return false; | |
} | |
} | |
/** | |
* Retrieves a value from cache with a specified key. Ignore expire time. | |
* @param string $id a key identifying the cached value | |
* @return mixed the value stored in cache, false if the value is not in the cache, or the dependency has changed. | |
*/ | |
public function forceGet($id) | |
{ | |
$value = parent::get($id); | |
return is_array($value) ? $value[0] : false; | |
} | |
/** | |
* Stores a value identified by a key into cache. | |
* If the cache already contains such a key, the existing value and | |
* expiration time will be replaced with the new ones. | |
* | |
* @param string $id the key identifying the value to be cached | |
* @param mixed $value the value to be cached | |
* @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. | |
* @param ICacheDependency $dependency dependency of the cached item. If the dependency changes, the item is labeled invalid. | |
* @return boolean true if the value is successfully stored into cache, false otherwise | |
*/ | |
public function set($id, $value, $expire = 0, $dependency = null) | |
{ | |
return parent::set($id, array($value, $expire == 0 ? $expire : time() + $expire), 0, $dependency); | |
} | |
/** | |
* Stores a value identified by a key into cache if the cache does not contain this key. | |
* Nothing will be done if the cache already contains the key. | |
* @param string $id the key identifying the value to be cached | |
* @param mixed $value the value to be cached | |
* @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. | |
* @param ICacheDependency $dependency dependency of the cached item. If the dependency changes, the item is labeled invalid. | |
* @return boolean true if the value is successfully stored into cache, false otherwise | |
*/ | |
public function add($id, $value, $expire = 0, $dependency = null) | |
{ | |
return parent::add($id, array($value, $expire == 0 ? $expire : time() + $expire), $expire, $dependency); | |
} | |
public function mget($ids) | |
{ | |
throw new CException('mget is not implemented'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment