redis的缓存雪崩,穿透,击穿介绍和如何解决(四)


Redis缓存雪崩

什么是缓存雪崩?

雪崩,雪一样的大面积崩。一些缓存设定用的是定时更新,这样的数据缓存是同时刷新,同时失效。比如失效时刚好跨零点跨天,商品开始抢购,大量请求进来,Redis缓存失效,只能全部打到DB去了,如果是小量请求,没慢日志,分库分表的数据库倒还扛得住,这时如果大量的话,1秒几千个请求进来,数据库必定挂了,再重启,再挂,这就是缓存雪崩

雪崩解决方法?

  1. 在批量设置Redis缓存时候,把每个Key的失效时间加上个随机值,time() + rand(1, 1000),保证不会都在同一时间失效
  2. 热点数据直接设置为不过期,数据有更新时直接刷新下缓存
  3. 用Redis集群部署,把数据分布在不同的Redis分片中,也可避免全部失效直打DB问题

Redis缓存穿透

什么是缓存穿透?

穿透,穿透过已有的缓存层,直接到DB。访问的数据在Redis缓存中没有,就去DB,DB也没有,没有设值到缓存,相当于每个请求都是透明的直接请求DB。比如请求id为负数,而用户不断发起请求,如果是黑客攻击,数据库压力就会很大,严重时数据库就会跨掉

穿透解决方法?

  1. 请求参数做基准校验,只要是请求的就要验证过滤,不信任任何方的请求参数,不符合就return

  2. 在接入层,通过nginx用limit_req_zone和limit_req_conn限制单个ip的每秒访问速率和并发数

  3. 读取DB中没有,也要设置key缓存为空值(Null),再加上一个短时的过期时间,比如30秒,让其可自动剔除。加短时间超时,一方面是为了防止请求大量不存在的key,内存会占用越来越大,消耗内存;一方面是为了防止正常情况下空值导致无法使用

  4. 用布隆过滤器(BloomFilter),简单介绍就是把有的key都放到一个大的集合里,它是一个二进制向量,存放的不是0就是1,当key检测不存在集合中,则是非法参数,直接return

    (1) 加入key到集合时,需要把key通过多个不同的算法计算对应位数,比如 JSHash算法,算出对应位数X,则设X为1

    (2) 检测是否在集合中,则通过算法获取对应的位数下值是否都为1 ,只要有一个为 0 则表示不存在集合中。所以,缺点就是集合越大,有可能出现误判,存在判为不存在

    (3) 例子:
    比如新建一个长度为8的布隆过滤器,一开始集合为空的时候,默认值都是0

    0 0 0 0 0 0 0 0

    加入一个key,通过Hash1算法 Hash1(key)= 2,Hash2(key) = 4,Hash3(key) = 7,把下标2,下标4,下标7标 注为1

    0 0 1 0 1 0 0 1

    验证时,则把key通过三个算法获取对应获取下标2,4,7,判断是否都为1,则key存在集合中

    (4) 主要直观代码:

    $keyBloom = "bloom:filter"; //集合名,总的key
    //加入布隆集合
    function add($item) {
       $hash1Pos = Hash1($item); 
    	$hash2Pos = Hash2($item);
       $hash3Pos = Hash3($item);
    	//用管道,合并一次IO
       $pipe = $redis->pipeline();
       $pipe->setbit($keyBloom, $hash1Pos, 1);
       $pipe->setbit($keyBloom, $hash2Pos, 1);
       $pipe->setbit($keyBloom, $hash3Pos, 1);
       $pipe->execute();
    }
    //判断是否存在集合中
    //return true | false
    public function has($item) {
       $hash1Pos = Hash1($item); 
       $hash2Pos = Hash2($item);
       $hash3Pos = Hash3($item);
    	//用管道,合并一次IO
       $pipe = $redis->pipeline();
       $pipe->getbit($keyBloom, $hash1Pos);
       $pipe->getbit($keyBloom, $hash2Pos);
       $pipe->getbit($keyBloom, $hash3Pos);
       $result = $pipe->execute();
       return !in_array(0, $result);
    }
    

什么是Redis缓存击穿?

击穿则是跟缓存雪崩有点类似,只不过雪崩是大面积,击穿是单个key非常热点,一直扛着高并发请求,当key在失效瞬间,持续的高并发请求直打DB,DB压力急剧上升

击穿解决方法?

  1. 既然是这么高热点数据,设置为不过期,有更新刷新下缓存

  2. 延时双重检测,首次获取缓存为空,先睡一小会,再获取缓存检测,还是为空再读DB设值到缓存,主要代码如下

    $result = $redis->get("testKey");
    //首次检测
    if(!isset($result)){
        //睡0.3秒,再检测
        usleep(3000);
        $result = $redis->get("testKey");
        if(!isset($result)){
           //还是没有,读db,设值到缓存
           $result = GetDataByDB("testKey");
           $redis->set("testKey", $result);
        }
    }
    return $result;
    
  3. 互斥锁,获取缓存没有,先获取操作锁,取得锁之后,读取DB设值到缓存

    互斥锁操作主要代码如下

    $now = time();
    //判断和设置时候要有原子性,用eval+lua实现,防止并发,脏读情况
    //设置锁为10秒自动失效
    $lua = <<<eof
        local key = KEYS[1]
        local val = KEYS[2]
        local lock = redis.call('get', key)
        if lock then
            if (tonumber(lock) > tonumber(val)) then
                return false
            end
            redis.call('set', key, val + 10)
            return true
        else
            redis.call('set', key, val + 10)
            return true
        end
    eof;
    $arr = ["lock", $now];
    //获取操作锁
    $result = $redis->eval($lua, $arr, count($arr));
    if ($result) {
        //
        //查询数据库然后把值同步到缓存逻辑~~~~~~
        //
        //移除锁
        $redis->del("lock");
        echo "读db完成";
    }
    else{
        echo "互斥中";
    }
    

简单总结:

  1. Qps高的,可用Redis集群,主从, 哨兵实现高可用,避免全盘崩溃

  2. 请求接入层限流,比如nginx的ip,连接数限制,超出阀值拉黑

  3. 采用AOF做热备,RDB做冷备,持久化保存,实现服务器稳健

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章