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