前言
先说下数据库的本质仍然还是基于磁盘来交互的,用过HDFS的都知道基于磁盘交互在单线程的情况下还是快的,在多线程情况下,速度呈倍数下降。所以在系统中redis正是诞生出来解决这个问题的。
一、缓存穿透
举个例子:比如说某位老哥开发了一个网站,然后这个网站非常的受欢迎,某一天突然遭到了黑客疯狂的攻击,他的这个攻击手段就是采用这个缓存穿透的原理。
大家都知道通常情况下,数据库的主键是从0开始递增的,是没有负数的,那么这位黑客就利用这点,他不断的用 ID 小于零的参数发请求过来。这位老哥刚开始是把网站的所有数据放到了 redis 缓存里面去,但是黑客是用 ID 小于零的数来请求,redis 缓存里面并没有这个 ID 小于零的数据,这样 redis 就查不到这个结果,一旦 redis 查不到结果就会去数据库中查,那么所有请求都会打到数据库,而且会一直打到数据库中去,因为 redis 缓存这层根本拦截不到这样的数据。
总而言之,缓存穿透就是来查找一个不存在的数据,从而避免了redis,大量的访问会直接到达数据库使得数据库直接宕机。
解放方案
- 不管数据库有没有查找到相应的数据,尽管是null为空值,都将此值写会redis
- 对请求的参数做合法性校验
- 使用过滤器来过滤请求
代码
if(list == null){
// key value 有效时间时间单位
redisTemplate . opsForValue( ). set( navKey,null,10, T imeUnit. MINUTES );
}else{
redisTemplate . opsForValue( ). set( navKey, result,7 ,TimeUnit. DAYS);
}
二、缓存雪崩
缓存雪崩其实有点像“升级版的缓存击穿”,缓存击穿是一个热点 key,缓存雪崩是一组热点 key。简而言之就是存在redis中的所有的数据在同一时刻消失了,这里可能有人说为什么一定要设置消失时间,我们都知道缓存是存在于内存之中的,如果一直叠加而不能GC垃圾回收,那么系统会报出oom的错误,所以这一步必不可少。
解决方案
- 随机设置这个缓存的失效时间,不让大量的 key 在同一时间失效,即在设置这个缓存的时候,可以将 key 的失效时间分散开。
- 数据库有限流方案,当达到了限流设置的参数,那么就会拒绝请求,从而保护了后台db。
- 加锁排队
加锁排队代码
//伪代码
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
String lockKey = cacheKey;
String cacheValue = CacheHelper.get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
synchronized(lockKey) {
cacheValue = CacheHelper.get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//这里一般是sql查询数据
cacheValue = GetProductListFromDB();
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
}
}
return cacheValue;
}
}
三、缓存击穿
大量的请求同时查询一个key时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。于是就会导致: 在缓存失效瞬间,有大量线程构建缓存,导致后端负载加剧,甚至可能让系统崩溃。这么说可能有一点模糊,举个例子,比如华为mate50刚上线,这时候因为查询存入了缓存中,但是当某一刻缓存自动失效之后,大量的访问就会直接越过redis。
解决方案
- 加锁,只能加锁,加同步锁或者分布式锁
加锁伪代码
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
四、本地锁和分布式锁
1、本地锁
本地锁也叫线程锁
我们都知道springboot是单进程模式,而进程又是由多个线程组成,那么线程锁的意思就是在这个进程中(代码段)只有一个线程可以通过,只有一个坑位。
2、分布式锁
先来看图片
这里每一个商品服务代表一个节点,由这样8个节点组成的集群,每一个节点上面都会开启商品服务(springboot),所以就会有8个进程,如果使用本地锁(线程锁)那么仍然会有8个请求到达我们的缓存或者数据库。
而分布式锁就是来避免这种情况,虽然8个请求并不多,且压力不大,但是想要锁住所有的进程只能使用分布式锁。
一般我们使用Redisson来操作分布式锁