[转]高并发访问下避免对象缓存失效引发Dogpile效应

避免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.
时间: 2024-11-08 17:26:14

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

高并发场景下的限流策略

高并发场景下的限流策略: 在开发高并发系统时,有很多手段来保护系统:缓存.降级.限流. 当访问量快速增长.服务可能会出现一些问题(响应超时),或者会存在非核心服务影响到核心流程的性能时, 仍然需要保证服务的可用性,即便是有损服务.所以意味着我们在设计服务的时候,需要一些手段或者关键数据进行自动降级,或者配置人工降级的开关. 缓存的目的是提升系统访问速度和增大系统处理的容量,可以说是抗高并发流量的银弹:降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉某些功能,等高峰或者问题解决后再打开:

缓存在高并发场景下的常见问题

缓存一致性问题 当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象.这就比较依赖缓存的过期和更新策略.一般会在数据发生更改的时,主动更新缓存中的数据或者移除对应的缓存. 缓存并发问题 缓存过期后将尝试从后端数据库获取数据,这是一个看似合理的流程.但是,在高并发场景下,有可能多个请求并发的去从数据库获取数据,对后端数据库造成极大的冲击,甚至导致 “雪崩”现象.此外,当某个缓存key在被更新时,同时也可能被大量请求在获取,

高并发场景下的缓存有哪些常见的问题?

一.缓存一致性问题 当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象. 这就比较依赖缓存的过期和更新策略.一般会在数据发生更改的时,主动更新缓存中的数据或者移除对应的缓存. 二.缓存并发问题 缓存过期后将尝试从后端数据库获取数据,这是一个看似合理的流程.但是,在高并发场景下,有可能多个请求并发的去从数据库获取数据,对后端数据库造成极大的冲击,甚至导致 "雪崩"现象. 此外,当某个缓存key在被更新时,同时也

高并发场景下使用缓存需要注意那些问题?

一.缓存一致性问题 当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象.这就比较依赖缓存的过期和更新策略.一般会在数据发生更改的时,主动更新缓存中的数据或者移除对应的缓存. 二.缓存并发问题 缓存过期后将尝试从后端数据库获取数据,这是一个看似合理的流程.但是,在高并发场景下,有可能多个请求并发的去从数据库获取数据,对后端数据库造成极大的冲击,甚至导致 "雪崩"现象.此外,当某个缓存key在被更新时,同时也可能

高并发场景下缓存+数据库双写不一致问题分析与解决方案设计

Redis是企业级系统高并发.高可用架构中非常重要的一个环节.Redis主要解决了关系型数据库并发量低的问题,有助于缓解关系型数据库在高并发场景下的压力,提高系统的吞吐量(具体Redis是如何提高系统的性能.吞吐量,后面会专门讲). 而我们在Redis的实际使用过程中,难免会遇到缓存与数据库双写时数据不一致的问题,这也是我们必须要考虑的问题.如果还有同学不了解这个问题,可以搬小板凳来听听啦. 一.数据库+缓存双写不一致问题引入 要讲数据库+缓存双写不一致的问题,就需要先讲一下这个问题是怎么发生的

面试常问问题:银行网上支付项目中怎么控制多线程高并发访问?

面试常问问题:银行网上支付项目中怎么控制多线程高并发访问? synchronized关键字主要解决多线程共享数据同步问题. ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题. ThreadLocal和Synchonized都用于解决多线程并发访问.但是ThreadLocal与synchronized有本质的区别: synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问.而ThreadLocal为每一个线程都提供了变量的副本,使 得每个线程在某一时

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

死锁对于DBA或是数据库开发人员而言并不陌生,它的引发多种多样,一般而言,数据库应用的开发者在设计时都会有一定的考量进而尽量避免死锁的产生.但有时因为一些特殊应用场景如高频查询,高并发查询下由于数据库设计的潜在问题,一些不易捕捉的死锁可能出现从而影响业务.这里为大家介绍由于设计问题引起的键查找死锁及相关的解决办法. 这里我们在测试的同时开启trace profiler跟踪死锁视图(locks:deadlock graph).(当然也可以开启跟踪标记,或者应用扩展事件(xevents)等捕捉死锁)

大话程序猿眼里的高并发(下)

大话程序猿眼里的高并发(下) 前言 高并发经常会发生在有大活跃用户量,用户高聚集的业务场景中,如:秒杀活动,定时领取红包等. 为了让业务可以流畅的运行并且给用户一个好的交互体验,我们需要根据业务场景预估达到的并发量等因素,来设计适合自己业务场景的高并发处理方案. 在电商相关产品开发的这些年,我有幸的遇到了并发下的各种坑,这一路摸爬滚打过来有着不少的血泪史,这里进行的总结,作为自己的归档记录,同时分享给大家. 服务器架构 业务从发展的初期到逐渐成熟,服务器架构也是从相对单一到集群,再到分布式服务.

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

死锁对于DBA或是数据库开发人员而言并不陌生,它的引发多种多样,一般而言,数据库应用的开发者在设计时都会有一定的考量进而尽量避免死锁的产生.但有时因为一些特殊应用场景如高频查询,高并发查询下由于数据库设计的潜在问题,一些不易捕捉的死锁可能出现从而影响业务.这里为大家介绍由于设计问题引起的键查找死锁及相关的解决办法. 这里我们在测试的同时开启trace profiler跟踪死锁视图(locks:deadlock graph).(当然也可以开启跟踪标记,或者应用扩展事件(xevents)等捕捉死锁)