避免Redis/Memcached缓存失效引发Dogpile效应

Redis/Memcached高并发访问下的缓存失效时可能产生Dogpile效应(Cache Stampede效应).

推荐阅读:高并发下的 Nginx 优化方案 http://www.linuxidc.com/Linux/2013-01/78791.htm

  • 避免Memcached缓存的Dogpile效应

    Memcached的read-through cache流程:客户端读取缓存,没有的话就由客户端生成缓存.
    Memcached缓存示例:

    $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服务器故障.

    避免这样的Dogpile效应,通常有两种方法:

    • 使用独立的更新进程
      使用独立的进程(比如: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.

[转]高并发访问下避免对象缓存失效引发Dogpile效应的更多相关文章

  1. 【高并发】高并发环境下构建缓存服务需要注意哪些问题?我和阿里P9聊了很久!

    写在前面 周末,跟阿里的一个朋友(去年晋升为P9了)聊了很久,聊的内容几乎全是技术,当然了,两个技术男聊得最多的话题当然就是技术了.从基础到架构,从算法到AI,无所不谈.中间又穿插着不少天马行空的想象 ...

  2. Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%。再往后,每提高0.1%,优化难度成指数级增长了。哪怕是千分之一,也直接影响用户体验,影响每天上万张机票的销售额。 在高并发场景下,提供了保证线程安全的对象、方法。比如经典的ConcurrentHashMap,它比起HashMap,有更小粒度的锁,并发读写性能更好。线程安全的StringBuilder取代S

    Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%.再往后,每提高0.1%,优化难度成指数级增长了.哪怕是千分之一,也直接影响用户体验,影响每天上万张机 ...

  3. php高并发状态下文件的读写

    php高并发状态下文件的读写   背景 1.对于PV不高或者说并发数不是很大的应用,不用考虑这些,一般的文件操作方法完全没有问题 2.如果并发高,在我们对文件进行读写操作时,很有可能多个进程对进一文件 ...

  4. Java高并发情况下的锁机制优化

    本文主要讲并行优化的几种方式, 其结构如下: 锁优化 减少锁的持有时间 例如避免给整个方法加锁 1 public synchronized void syncMethod(){ 2 othercode ...

  5. Linux的虚拟内存管理-如何分配和释放内存,以提高服务器在高并发情况下的性能,从而降低了系统的负载

    Linux的虚拟内存管理有几个关键概念: Linux 虚拟地址空间如何分布?malloc和free是如何分配和释放内存?如何查看堆内内存的碎片情况?既然堆内内存brk和sbrk不能直接释放,为什么不全 ...

  6. HttpClient在高并发场景下的优化实战

    在项目中使用HttpClient可能是很普遍,尤其在当下微服务大火形势下,如果服务之间是http调用就少不了跟http客户端找交道.由于项目用户规模不同以及应用场景不同,很多时候可能不需要特别处理也. ...

  7. 【高并发】ReadWriteLock怎么和缓存扯上关系了?!

    写在前面 在实际工作中,有一种非常普遍的并发场景:那就是读多写少的场景.在这种场景下,为了优化程序的性能,我们经常使用缓存来提高应用的访问性能.因为缓存非常适合使用在读多写少的场景中.而在并发场景中, ...

  8. ql Server 高频,高并发访问中的键查找死锁解析

    死锁对于DBA或是数据库开发人员而言并不陌生,它的引发多种多样,一般而言,数据库应用的开发者在设计时都会有一定的考量进而尽量避免死锁的产生.但有时因为一些特殊应用场景如高频查询,高并发查询下由于数据库 ...

  9. Sql Server 高频,高并发访问中的键查找死锁解析

    死锁对于DBA或是数据库开发人员而言并不陌生,它的引发多种多样,一般而言,数据库应用的开发者在设计时都会有一定的考量进而尽量避免死锁的产生.但有时因为一些特殊应用场景如高频查询,高并发查询下由于数据库 ...

随机推荐

  1. 关于变量在for循环内外定义的思考

    在c#或java里面,我们通常会这样写for循环: ;i<;i++) { Console.WriteLine(i); } 前一阵子,我突发奇想,如果按照下面这样写,会不会比上面的运行效率高一些: ...

  2. 认识C中的结构体

    C中结构体是另外一种表示数据形式的方式,结构体中可以表示C中的基本数据形式,如int,double....结构体可以让我们更好的表示数据.下面来看看结构体. 说到结构体首先要了解的是它的申明形式,要申 ...

  3. nodejs的mysql模块学习(五)数据库连接配置之SSL

    SSL选项 在SSL连接选项中需要一个字符串 或者对象 当是字符串的时候 将使用预定义的SSL配置文件 "Amazon RDS" 只有这一个预定义配置文件 用来连接到亚马逊RDS服 ...

  4. [经典算法] 排列组合-N元素集合的所有子集(一)

    题目说明: 给定一组数字或符号,产生所有可能的集合(包括空集合),例如给定1 2 3,则可能的集合为:{}.{1}.{1,2}.{1,2,3}.{1,3}.{2}.{2,3}.{3}. 题目解析: 如 ...

  5. 【Python千问 2】Python核心编程(第二版)-- 欢迎来到Python世界

    1.1 什么是Python 继承了传统编译语言的强大性和通用性,同时也借鉴了简单脚本和解释语言的易用性. 1.2 起源 来源于某个项目,那些程序员利用手边现有的工具辛苦工作着,他们设想并开发了更好的解 ...

  6. 【开源项目13】Volley框架 以及 设置request超时时间

    Volley提供了优美的框架,使android程序网络访问更容易.更快. Volley抽象实现了底层的HTTP Client库,我们不需关注HTTP Client细节,专注于写出更加漂亮.干净的RES ...

  7. 【Android 界面效果40】Android4.0-Fragment框架实现方式剖析(一)

    经过反复的学习对比,个人觉得带着问题学习新知是最有效的学习方式,因此文本就以提问的方式来讲述Fragment框架实现方式. 1.什么是Fragment? Fragment包含在Activity中,Fr ...

  8. 【Java的JNI快速学习教程】

    1. JNI简介 JNI是Java Native Interface的英文缩写,意为Java本地接口. 问题来源:由于Java编写底层的应用较难实现,在一些实时性要求非常高的部分Java较难胜任(实时 ...

  9. Oracle基础 (十四)其他函数

    转换函数: TO_DATE:转换为日期 --将字符串转换为日期 SELECT TO_DATE('2014-12-31', 'yyyy-mm-dd') FROM DUAL; SELECT TO_DATE ...

  10. Lnmp下安装memcached

            Lnmp下安装memcached 1.先安装 libevent,再安装 Memcached主程序 # tar xf libevent-2.0.21-stable.tar.gz # cd ...