缓存失效、缓存穿透问题解决方案

缓存失效(缓存雪崩): 
  引起这个原因的主要因素是高并发下,我们一般设定一个缓存的过期时间时,可能有一些会设置5分钟啊,10分钟这些;并发很高时可能会出在某一个时间同时生成了很多的缓存,并且过期时间在同一时刻,这个时候就可能引发——当过期时间到后,这些缓存同时失效,请求全部转发到DB,DB可能会压力过重。 
  处理方法: 一个简单方案就是将缓存失效时间分散开,不要所以缓存时间长度都设置成5分钟或者10分钟;比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 缓存失效时产生的雪崩效应,将所有请求全部放在数据库上,这样很容易就达到数据库的瓶颈,导致服务无法正常提供。尽量避免这种场景的发生。 

  和缓存击穿不同的是,  缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

缓存穿透: 
  出现场景:指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

  当在流量较大时,出现这样的情况,一直请求DB,很容易导致服务挂掉。 
处理方法: 
  方法1.在封装的缓存SET和GET部分增加个步骤,如果查询一个KEY不存在,就已这个KEY为前缀设定一个标识KEY;以后再查询该KEY的时候,先查询标识KEY,如果标识KEY存在,就返回一个协定好的非false或者NULL值,然后APP做相应的处理,这样缓存层就不会被穿透。当然这个验证KEY的失效时间不能太长。

  方法2.如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,一般只有几分钟。 

public function getArticles($key)
{
    $expire = 60 * 3;
    $data = Cache::get($key);
    //注意:此处使用is_null来判断而不是直接使用 (!$data)来判断。
    //使用 (!$data)来判断的弊端是:如果$data的值为空字符串或者空数组,此处也是不成立的,会继续执行查询DB的语句,造成缓存穿透
    if (!is_null($data)) {
        return $data;
    }
    $data = $this->searchDB();
    Cache::put($key, $data, $expire);
    return $data;
}

    这样处理的原因是,即使当前查询的key为空字符串,或者空数组,结果也会被缓存起来。当下一次访问时会直接返回,不会造成缓存穿透

  方法3.采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

             如果是非法攻击时,接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;

缓存并发: 
  出现场景:当网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题。 
  处理方法:,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。

    我们可以使用redis的setNx()
setNx($key) 的作用类似于set($key) ,setNx的意思为 set Not Exists 如果$key不存在则设置,存在则不进行任何操作. 设置成功设置返回1,说明当前的请求获得了当前的操作权限,设置失败返回0,说明此资源已经被其他请求获得。使用代码实现的话,思路如下:

  1. 给存入缓存的数据增加一个过期时间字段暂时给这个字段起名字叫$data['expire'](这个过期时间要短于实际的缓存过期时间),方便在缓存过期前执行加锁和缓存更新。
  2. 如果$data['expire']达到过期时间,则执行加锁以及缓存更新。
  3. 此时如果有其他请求进入则返回更新之前的数据。

代码如下:

    

public function getArticlesLock($key)
{
    $time = time();
    $expire = 10 * 2;
    $lockKey = 'lock:k';
    $data = Cache::get($key);

    if (!is_null($data)) {
        //缓存未过期
        if ($data['expire'] > time()){
            return $data['data'];
        }
        //加锁失败说明已经有请求执行加锁,返回之前的缓存数据
        if (!Redis::setnx($lockKey,1)) {
            return $data['data'];
        }
    }
    sleep(3);
    $datat = $this->searchDB();
    $data = [
        'data' => $datat,
        'expire' => $time + $expire - 10
    ];
    $r = Cache::put($key, $data, $expire);
    //解锁
    Redis::del($lockKey);
    return $data['data'];
}

  当然此处也可以使用set()来代替setnx()加锁,以及使用lua脚本解锁。

三、缓存击穿

      描述:

      缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

      解决方案:

  1. 设置热点数据永远不过期。
  2. 加互斥锁,互斥锁参考代码如下
  3.   说明:

              1)缓存中有数据,直接走上述代码13行后就返回结果了

             2)缓存中没有数据,第1个进入的线程,获取锁并从数据库去取数据,没释放锁之前,其他并行进入的线程会等待100ms,再重新去缓存取数据。这样就防止都去数据库重复取数据,重复往缓存中更新数据情况出现。

              3)当然这是简化处理,理论上如果能根据key值加锁就更好了,就是线程A从数据库取key1的数据并不妨碍线程B取key2的数据,上面代码明显做不到这点。

原文地址:https://www.cnblogs.com/flzs/p/14283480.html