• [转]高并发访问下避免对象缓存失效引发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.
  • 相关阅读:
    Contest Record
    Work at DP
    波兰题目补全计划
    BZOJ #3746: [POI2015]Czarnoksiężnicy okrągłego stołu 动态规划
    【HEOI 2018】制胡窜
    【HEOI 2018】林克卡特树
    省选之前的未完成的计划(截至到省选)
    小学半平面交
    小学扩展欧拉定理
    【复习】高斯消元解图上期望概率
  • 原文地址:https://www.cnblogs.com/longhao/p/4230566.html
Copyright © 2020-2023  润新知