Use "Lock"
In addition to using a standalone update process, we can also add "lock" to allow only one client request to update the cache at a time to avoid the dogpile effect.
The process is probably like this:
- A requested cache is not hit
- A request "lock" the cache key
- b requested Cache not hit
- b requests need to wait until "lock" is released
- A request is complete and the "lock" is released
- B Request Cache Hit (due to the operation of a)
memcached examples of using "locks":
function Get ($key) { global $mc; $data = $MC->get ($key); Check if cache exists if ($MC->getresultcode () = = = memcached::res_success) { return $data; } Add Locking $MC->add (' Lock: '. $key, ' locked ', +); if ($MC->getresultcode () = = = memcached::res_success) { $data = Generatedata (); $MC->set ($key, $data,); } else { while (1) { usleep (500000); $data = $MC->get ($key); if ($data!== false) {break ; } } } return $data;} $data = Get (' Cached_key '); Var_dump ($data);
There is a flaw in the above approach, that is, when the cache fails, all requests need to wait for a request to complete the cache update, which will undoubtedly increase the pressure on the server.
This is relatively good if the cache update can be triggered for a period of time before the data fails, or if the cache fails to return only the appropriate state for the client to handle itself according to the return state.
The following get method is to return the corresponding state handled by the client:
Class Cache {Const RES_SUCCESS = 0; Const GENERATEDATA = 1; Const NOTFOUND = 2; Public function __construct ($memcached) {$this->MC = $memcached; Public function Get ($key) {$data = $this->mc->get ($key); Check if cache exists if ($this->mc->getresultcode () = = = memcached::res_success) {$this->_s Etresultcode (cache::res_success); return $data; }//Add locking $this->mc->add (' Lock: '. $key, ' locked ', 20); if ($this->mc->getresultcode () = = = memcached::res_success) {$this->_setresultcode (cache::generatedata ); return false; } $this->_setresultcode (Cache::notfound); return false; } Private Function _setresultcode ($code) {$this->code = $code; } public Function Getresultcode () {return $this->code; The public function set ($key, $data, $expiry) {$this->mc->set ($key, $dATA, $expiry); }} $cache = new cache ($MC), $data = $cache->get (' Cached_key '); switch ($cache->getresultcode ()) {case Cache::res_ SUCCESS://... break; Case Cache::generatedata://Generate Data ... $cache->set (' Cached_key ', Generatedata (), 30); Break Case Cache::notfound://not found ... break;}When the above memcached cache fails, only one client request will return the Cache::generatedata state, and the others will return Cache::notfound. The client can do the appropriate processing by detecting these states.
Note that the TTL value for "lock" should be greater than generatedata () consumption time, but should be less than the TTL value of the actual cached object.
Avoid the dogpile effect of Redis cacheRedis Normal Read-through Cache Example:
$redis = new Redis (), $redis->connect (' 127.0.0.1 ', 6379), $data = $redis->get (' Hot_items '); if ($data = = = False) {
//calculate hot items from MySQL, says:it takes seconds for this process $data = Expensive_database_call ();
//store the data with a minute expiration $redis->setex ("Hot_items", Max, $data);} Var_dump ($data);
As with the memcached cache, the Dogpile effect can also be triggered when the Redis cache fails in high concurrency.
Below is an example of how Redis avoids the dogpile effect by using "lock":
$redis = new Redis (), $redis->connect (' 127.0.0.1 '); $expiry =///Cached 600S$RECALCULATED_AT = 100s $lock _length =; Lock-key expiry 20s$data = $redis->get ("Hot_items"), $ttl = $redis->get ("Hot_items"); if ($ttl <= $ Recalculated_at && $redis->setnx (' Lock:hot_items ', true)) { $redis->expire (' Lock:hot_items ', $ Lock_length); $data = Expensive_database_call (); $redis->setex (' Hot_items ', $expiry, $data);} Var_dump ($data);
The process above is this:
- Normally gets the cached data of key hot_items and also gets the TTL (the time remaining from expiration)
- The above Hot_items expiration time is set to 600s, but when the Hot_items is ttl<=100s, the cached update process is triggered
$redis->setnx(‘lock:hot_items‘, true)Try to create a key as a "lock". If the key already exists, SETNX will not do any action and the return value is false, so only one client will return a true value to the IF statement to update the cache.
- Set the 20s expiration time for the key as "lock", in case the PHP process crashes or processing expires, allow another process to update the cache after the key as "lock" expires.
- If the Expensive_database_call () is called in the IF statement, the most recent data is saved normally to Hot_items.