PHP cache slam defense using a semaphore to prevent dogpile effect (aka clobbering updates, stampeding herd or Slashdot effect).
Problem: too many requests hit your website at the same time while it tries to regenerate same content slamming your database, eg. when cache expired.
Solution: first request generates new content while all the subsequent requests get (stale) content from cache until it's refreshed by the first request.
Read http://www.sobstel.org/blog/preventing-dogpile-effect/ for more details.
In composer.json file:
"require": {
"sobstel/metaphore": "2.0.*"
}
or just composer require sobstel/metaphore
use Metaphore\Cache;
use Metaphore\Store\MemcachedStore;
// initialize $memcached object (new Memcached())
$cache = new Cache(new MemcachedStore($memcached));
$cache->cache('key', function() {
// generate content
}, 30);-
__construct(ValueStoreInterface $valueStore, LockManager $lockManager = null) -
cache($key, callable $callable, [$ttl, [$onNoStaleCacheCallable]])- returns result -
delete($key) -
getValue($key)- returns Value object -
setResult($key, $result, Ttl $ttl)- sets result (without anti-dogpile-effect mechanism) -
onNoStaleCache($callable) -
getValueStore() -
getLockManager()
Cache values and locks can be handled by different stores.
$valueStore = new Metaphore\MemcachedStore($memcached);
$lockStore = new Your\Custom\MySQLLockStore($connection);
$lockManager = new Metaphore\LockManager($lockStore);
$cache = new Metaphore\Cache($valueStore, $lockManager);By default - if no 2nd argument passed to Cache constructor - value store is used as a lock store.
Sample use case might be to have custom MySQL GET_LOCK/RELEASE_LOCK for locks and still use in-built Memcached store for storing values.
You can pass simple integer value...
$cache->cache('key', callback, 30); // cache for 30 secs.. or use more advanced Metaphore\TTl object, which gives you control over grace period and lock ttl.
// $ttl, $grace_ttl, $lock_ttl
$ttl = new Ttl(30, 60, 15);
$cache->cache('key', callback, $ttl);$ttl- regular cache time (in seconds)$grace_ttl- grace period, how long to allow to serve stale content while new one is being generated (in seconds), similar to HTTP's stale-while-revalidate, default is 60s$lock_ttl- lock time, how long to prevent other request(s) from generating same content, default is 5s
Ttl value is added to current timestamp (time() + $ttl).
In rare situations, when cache gets expired and there's no stale (generated earlier) content available, all requests will start generating new content.
You can add listener to catch this:
$cache->onNoStaleCache(function (NoStaleCacheEvent $event) {
Logger::log(sprintf('no stale cache detected for key %s', $event->getKey()));
});You can also affect value that is returned:
$cache->onNoStaleCache(function (NoStaleCacheEvent $event) {
$event->setResult('new custom result');
});Run all tests: phpunit.
If no memcached or/and redis installed: phpunit --exclude-group=notisolated or phpunit --exclude-group=memcached,redis.
