redis 缓存穿透、击穿、雪崩

客户端请求数据,控制器接收用户请求调用模型查询用户所需的数据返回给客户端,业务逻辑非常简单,但是当成千上万的客户频繁的请求数据呢?那样系统会变得很慢,客户的请求长时间得不到回应,这时候就要使用缓存技术了,这样业务逻辑就变成了前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果。后续的查询都是从缓存服务器中返回数据,降低了高并发而给DB数据库的压力,避免DB宕机。

缓存穿透

缓存穿透指的是,当我们访问某个缓存 key 时,若此 key 不存在于缓存中,则会去查DB数据库,但也没有数据,而后续请求都直接打到DB数据库上。在流量大时,可能DB数据库就挂掉了。 缓存和数据库都查询不到 这条数据的现象我们称为 缓存穿透 。要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决办法

  • 给缓存手动设置一个默认值,不去查询DB数据库。
  • 把查询DB数据的结果都放入缓存。 即使查询返回的为空,仍然将这个空结果存入到缓存中 ,但是它的过期时间非常的短,如30秒,最长不会超过5分钟。这样可以防止攻击者反复暴力攻击。
  • 使用布隆过滤器(Bloom Filter),这个也能很好地防止缓存穿透。原理就是利用高效的数据结构和算法快速判断出你这个 Key 是否在 DB 中存在,不存在你 return 就好了,存在你就去查了 DB 刷新 KV 再 return。

这样处理的原因是,即使当前查询的 key 为 空字符串 ,或者 空数组 非null 值,结果也会被缓存起来。后续请求进来发现已经有缓存了,就直接走缓存。


缓存击穿

在平常高并发的系统中,大量的请求同时查询一个 key 时,此时这个key正好缓存时间到期,失效了,就会导致大量的请求都打到DB数据库上面去。这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。 缓存中没有数据值,但数据库中有此值 (一般是缓存时间到期),这种现象我们称为缓存击穿。

解决办法

  • 设置热点数据永远不过期。
  • 使用互斥锁。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去查询数据库,而是先使用缓存工具的某些带成功操作返回值的操作(比如 Redis 的 SETNX 或者 Memcache 的 ADD)去 set 一个 mutex key,当操作返回成功时,再进行查询数据库的操作并回设缓存;否则,就重试整个 get 缓存的方法。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。

若系统的并发很高,当缓存过期时,则大量的请求会穿透缓存,同时到DB中查询,那我们可以设置缓存当缓存过期时,只去DB中请求一次并缓存吗?可以,我们可以使用 redis 的 setnx() ,意思是(set Not Exists),如果$key不存在则设置,存在则不进行任何操作。设置成功设置返回1,说明当前的请求获得了当前的操作权限,设置失败返回0,说明此资源已经被其他请求获得。

给存入缓存的数据增加一个过期时间字段暂时:

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'];
}


缓存雪崩

某时刻发生大规模的缓存失效 的情况,比如你的缓存服务宕机了,会有大量的请求进来直接打到DB数据库上面,DB数据库压力瞬间增大,结果就是DB数据库支撑不住而崩溃。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决办法

  • 设置热点数据永远不过期。
  • 设置不同的失效时间。为了避免这些热点的数据集中失效,那么我们在设置缓存过期时间的时候,我们让他们失效的时间错开。比如在一个基础的时间上加上或者减去一个范围内的随机值。
  • 缓存的高可用性。缓存层设计成高可用,防止缓存大面积故障。即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 Redis Sentinel 和 Redis Cluster 都实现了高可用。
  • 缓存备份和快速预热。比如:Redis持久化机制,尽快恢复缓存集群。一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。


缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

  • 直接写个缓存刷新页面,上线时手工操作下;
  • 数据量不大,可以在项目启动的时候自动进行加载;

目的就是在系统上线前,将数据加载到缓存中。


互斥锁

Laravel8 框架中位置: vendor\laravel\framework\src\Illuminate\Cache RedisLock.php 文件中Redis 锁类,在这个类中,使用 acquire() 方法获得互斥的 Redis 锁,使用 release() 方法释放锁。

use Illuminate\Support\Facades\Redis;
use Illuminate\Cache\RedisLock;
function RedisLockTest()
{
    //获取redis实例
    $redis = Redis::connection();
    $key = 'redis_test_key';

    //获取redis锁实例
    $redisLock = new RedisLock($redis, $key . '_lock', 30);
    $res = $redis->get($key);
    if (empty($res)) 
    {
        //拿到互斥锁
        if ($redisLock->acquire()) 
	{
            //模拟从数据库中获取数据的过程
            sleep(5);
            $value = date('Y-m-d H:i:s');

            //更新缓存,过期时间可以根据实际情况调整
            $redis->setex($key, 60, $value);

            //释放锁
            $redisLock->release();
            return $value;
        } 
	else
	{
            //等待2秒,然后重新获取缓存值,让其他获取到锁的进程取得数据并设置缓存,等待时间可以根据实际情况调整
            sleep(2);
            return $this->RedisLockTest();
        }
    }
    else 
    {
        return $res;
    }
}



在Laravel 框架中读取缓存的方法有: Cache::get($key) Cache::remember($key,$seconds,Closure $callback)

Cache::get 返回值
  • 当 key 键存在,并且值是 非null 值的时候,返回此值。
  • 当 key 键存在,并且值是 null 值的时候,返回 null
  • 当 key 键不存在,返回 null
Cache::remember 返回值
  • 当 key 键存在,并且值是 非null 值的时候,返回此值。
  • 当 key 键存在,并且值是 null 值的时候,运行第三个参数 Closure $callback (闭包函数)可以查询数据库获得值。若也是null,也会被储存。
  • 当 key 键不存在,运行第三个参数 Closure $callback (闭包函数)可以查询数据库获得值。若也是null,也会被储存。
Cache::has 返回值

在Laravel 框架中判断是否存在值方法有: Cache::has($key)

  • 当 key 键存在,并且值是 非null、非false 值的时候,返回 true
  • 当 key 键存在,并且值是 null 或 false 值的时候,返回 false
  • 当 key 键不存在,返回 false

Laravel8 框架中源码位置: vendor\laravel\framework\src\Illuminate\Cache Repository.php 文件中

public function has($key)
{
        return ! is_null($this->get($key));
}



public function get($key, $default = null)
    {
        if (is_array($key)) {
            return $this->many($key);
        }

        $value = $this->store->get($this->itemKey($key));

        // If we could not find the cache value, we will fire the missed event and get
        // the default value for this cache value. This default could be a callback
        // so we will execute the value function which will resolve it if needed.
        if (is_null($value)) {
            $this->event(new CacheMissed($key));

            $value = value($default);
        } else {
            $this->event(new CacheHit($key, $value));
        }

        return $value;
}



public function remember($key, $ttl, Closure $callback)
{
        $value = $this->get($key);

        // If the item exists in the cache we will just return this immediately and if
        // not we will execute the given Closure and cache the result of that for a
        // given number of seconds so it's available for all subsequent requests.

        if (!is_null($value)) {
            return $value;
        }

        $this->put($key, $value = $callback(), $ttl);

        return $value;
}

public function put($key, $value, $ttl = null)
{
        if (is_array($key)) {
            return $this->putMany($key, $value);
        }

        if ($ttl === null) {
            return $this->forever($key, $value);
        }

        $seconds = $this->getSeconds($ttl);

        if ($seconds <= 0) { return $this->forget($key);
        }

        $result = $this->store->put($this->itemKey($key), $value, $seconds);

        if ($result) {
            $this->event(new KeyWritten($key, $value, $seconds));
        }

        return $result;
}