Skip to content

Instantly share code, notes, and snippets.

@m8rge
Last active December 11, 2015 05:48
Show Gist options
  • Save m8rge/4554478 to your computer and use it in GitHub Desktop.
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!
<?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.
*/
<?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