Redis/Memcached高併發訪問下的緩存失效時可能產生Dogpile效應(Cache Stampede效應).mysql
推薦閱讀:高併發下的 Nginx 優化方案 http://www.linuxidc.com/Linux/2013-01/78791.htmlinux
Memcached的read-through cache流程:客戶端讀取緩存,沒有的話就由客戶端生成緩存.
Memcached緩存示例:web
$mc = new Memcached(); $mc->addServers(array( array('127.0.0.1', 11211, 40), array('127.0.0.1', 11212, 30), array('127.0.0.1', 11213, 30) )); $data = $mc->get('cached_key'); if ($mc->getResultCode() === Memcached::RES_NOTFOUND) { $data = generateData(); // long-running process $mc->set('cached_key', $data, time() + 30); } var_dump($data);
假如上面的generateData()是耗時3秒(或更長時間)的運算或數據庫操做.當緩存服務器不可用(好比:緩存實例宕機,或網絡緣由)或是緩存失效瞬間,若是剛好有大量的訪問請求,那就會出現機器CPU消耗或數據庫操做次數短期內急劇攀升,可能會引起數據庫/Web服務器故障.redis
避免這樣的Dogpile效應,一般有兩種方法:sql
Memcached使用」鎖」的示例:數據庫
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', 20); if ($mc->getResultCode() === Memcached::RES_SUCCESS) { $data = generateData(); $mc->set($key, $data, 30); } else { while(1) { usleep(500000); $data = $mc->get($key); if ($data !== false){ break; } } } return $data; } $data = get('cached_key'); var_dump($data);
上面的處理方法有個缺陷,就是緩存失效時,全部請求都須要等待某個請求完成緩存更新,那樣無疑會增長服務器的壓力.
若是能在數據失效以前的一段時間觸發緩存更新,或者緩存失效時只返回相應狀態讓客戶端根據返回狀態自行處理,那樣會相對比較好.緩存
下面的get方法就是返回相應狀態由客戶端處理:服務器
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->_setResultCode(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; } 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; }
上面的memcached緩存失效時,只有一個客戶端請求會返回Cache::GenerateData狀態,其它的都會返回Cache::NotFound.客戶端可經過檢測這些狀態作相應的處理.
須要注意的是:」鎖」的TTL值應該大於generateData()消耗時間,但應該小於實際緩存對象的TTL值.網絡
Redis正常的read-through cache示例:併發
$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 10 seconds for this process $data = expensive_database_call(); // store the data with a 10 minute expiration $redis->setex("hot_items", 600, $data); } var_dump($data);
跟Memcached緩存同樣,高併發狀況下Redis緩存失效時也可能會引起Dogpile效應.
下面是Redis經過使用」鎖」的方式來避免Dogpile效應示例:
$redis = new Redis(); $redis->connect('127.0.0.1'); $expiry = 600; // cached 600s $recalculated_at = 100; // 100s left $lock_length = 20; // 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);
上面的流程是這樣的:
$redis->setnx('lock:hot_items', true)
嘗試建立一個key做爲」鎖」.若key已存在,setnx不會作任何動做且返回值爲false,因此只有一個客戶端會返回true值進入if語句更新緩存.