• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • 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\CacheRedisLock.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\CacheRepository.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;
    }