[轉]高併發訪問下避免對象緩存失效引起Dogpile效應

避免Redis/Memcached緩存失效引起Dogpile效應

Redis/Memcached高併發訪問下的緩存失效時可能產生Dogpile效應(Cache Stampede效應).mysql

推薦閱讀:高併發下的 Nginx 優化方案 http://www.linuxidc.com/Linux/2013-01/78791.htmlinux

  • 避免Memcached緩存的Dogpile效應

    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

    • 使用獨立的更新進程
      使用獨立的進程(好比:cron job)去更新緩存,而不是讓web服務器即時更新數據緩存.舉個例子:一個數據統計須要每五分鐘更新一次(可是每次計算過程耗時1分鐘),那麼可使用cron job去計算這個數據,並更新緩存.這樣的話,數據永遠都會存在,即便不存在也不用擔憂產生dogpile效應,由於客戶端沒有更新緩存的操做.這種方法適合不須要即時運算的全局數據.但對用戶對象,朋友列表,評論之類的就不太適用.
    • 使用」鎖」
      除了使用獨立的更新進程以外,咱們也能夠經過加」鎖」,每次只容許一個客戶端請求去更新緩存,以免Dogpile效應.
      處理過程大概是這樣的:

       

      1. A請求的緩存沒命中
      2. A請求」鎖住」緩存key
      3. B請求的緩存沒命中
      4. B請求須要等待直到」鎖」釋放
      5. A請求完成,而且釋放」鎖」
      6. B請求緩存命中(因爲A的運算)

      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緩存的Dogpile效應

      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);
      

      上面的流程是這樣的:

      1. 正常獲取key爲hot_items的緩存數據,同時也獲取TTL(距離過時的剩餘時間)
      2. 上面hot_items過時時間設置爲600s,但當hot_items的TTL<=100s時,就觸發緩存的更新過程
      3. $redis->setnx('lock:hot_items', true)嘗試建立一個key做爲」鎖」.若key已存在,setnx不會作任何動做且返回值爲false,因此只有一個客戶端會返回true值進入if語句更新緩存.
      4. 給做爲」鎖」的key設置20s的過時時間,以防PHP進程崩潰或處理過時時,在做爲」鎖」的key過時以後容許另外的進程去更新緩存.
      5. if語句中調用expensive_database_call(),將最新的數據正常保存到hot_items.
相關文章
相關標籤/搜索