[转]高并发访问下避免对象缓存失效引起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.
相关文章
相关标签/搜索