https://blog.csdn.net/fcvtb/article/details/89478554
前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果
缓存穿透
描述:
key对应的数据在缓存和数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。
在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
方案1: 对于数据库中不存在的数据, 也对其在缓存中设置默认值Null, 为避免占用资源, 一般过期时间会比较短
方案2: 可以设置一些过滤规则, 如布隆过滤器。在数据库查询之前将数据进行过滤, 如果发现数据不存在, 则不再进行数据库查询, 以此来减小数据库的访问压力
缓存击穿
描述:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决方案:
方案一:使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果
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;
}
}
方案二:后台刷新
后台启动一个常驻进程,用于在缓存失效前主动更新缓存中的数据,在缓存失效前后台进程刷新一下缓存中的数据,保证数据永远不会过期。
后台定义一个job(定时任务)专门主动更新缓存数据.比如,一个缓存中的数据过期时间是30分钟,那么job每隔29分钟定时刷新数据(将从数据库中查到的数据更新到缓存中).
方案三:检查更新
get请求主动更新缓存:将缓存key的过期时间点一起保存到缓存里(这个方法有很多,拼接,添加新字段,单独key皆可),在每次执行get操作后,都将get出来的缓存过期时间和当前系统时间做一次对比,如果缓存时间减去当前系统时间小于等于某个设定值后,则主动更新缓存。这样就能保证缓存中的数据始终是最新的。
缺点:在某个特殊时刻时,例如在缓存即将过期时没有get请求,导致缓存已经过期,恰好此时有大量并发请求过来,那就惨了。当然这种情况比较极端,但也是有可能的。
方案四:分级缓存
采用L1(一级缓存)和L2(二级缓存)的缓存方式,L1缓存失效时间短,L2缓存失效时间长。请求优先从L1获取数据,如果L1未命中则加锁,只有1个线程获取到锁,这个线程再从数据库中读取数据并将数据更新到L1和L2中,而其他线程依旧从L2缓存中获取数据并返回。
缺点:这种方式主要是通过避免缓存同时失效并结合锁机制实现。那么就会出现一个问题,有一瞬间L2可能会存在脏数据。就是当数据更新时,L1的缓存被淘汰,但是L2未被淘汰。且这种方案可能会造成额外的缓存空间浪费
缓存雪崩
描述:
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,而查询数据量巨大,也会给后端系统(比如DB)带来很大压力。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
1、优化缓存过期时间
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。避免大量的 key 在同一时刻同时失效。
2、缓存不过期
Redis 中保存的 key 永不失效,这样就不会出现大量缓存同时失效的问题,但是随之而来的就是Redis 需要更多的存储空间。
3、使用互斥锁重建缓存
在高并发场景下,为了避免大量的请求同时到达存储层查询数据、重建缓存,可以使用互斥锁控制,如根据 key 去缓存层查询数据,当缓存层为命中时,对 key 加锁,然后从存储层查询数据,将数据写入缓存层,最后释放锁。若其他线程发现获取锁失败,则让线程休眠一段时间后重试。对于锁的类型,如果是在单机环境下可以使用 Java 并发包下的 Lock,如果是在分布式环境下,可以使用分布式锁(Redis 中的 SETNX 方法)。
分布式环境下使用Redis 分布式锁实现缓存重建,优点是设计思路简单,对数据一致性有保障;缺点是代码复杂度增加,有可能会造成用户等待。假设在高并发下,缓存重建期间 key 是锁着的,如果当前并发 1000 个请求,其中 999 个都在阻塞,会导致 999 个用户请求阻塞而等待。
4、异步重建缓存
在这种方案下构建缓存采取异步策略,会从线程池中获取线程来异步构建缓存,从而不会让所有的请求直接到达存储层,该方案中每个Redis key 维护逻辑超时时间,当逻辑超时时间小于当前时间时,则说明当前缓存已经失效,应当进行缓存更新,否则说明当前缓存未失效,直接返回缓存中的 value 值。如在Redis 中将 key 的过期时间设置为 60 min,在对应的 value 中设置逻辑过期时间为 30 min。这样当 key 到了 30 min 的逻辑过期时间,就可以异步更新这个 key 的缓存,但是在更新缓存的这段时间内,旧的缓存依然可用。这种异步重建缓存的方式可以有效避免大量的 key 同时失效。
5、保持缓存层的高可用性
使用Redis 哨兵模式或者Redis 集群部署方式,即便个别Redis 节点下线,整个缓存层依然可以使用。除此之外,还可以在多个机房部署 Redis,这样即便是机房死机,依然可以实现缓存层的高可用。
6、限流降级组件
无论是缓存层还是存储层都会有出错的概率,可以将它们视为资源。作为并发量较大的分布式系统,假如有一个资源不可用,可能会造成所有线程在获取这个资源时异常,造成整个系统不可用。降级在高并发系统中是非常正常的,比如推荐服务中,如果个性化推荐服务不可用,可以降级补充热点数据,不至于造成整个推荐服务不可用。常见的限流降级组件如 Hystrix、SenTInel 等。