目录
一、缓存雪崩
1.1 解决缓存雪崩问题
二、缓存穿透
2.1 解决缓存穿透
三、缓存击穿
3.1 解决缓存击穿
3.2 如何保证数据一致性问题?
一、缓存雪崩
缓存雪崩是指短时间内,有大量缓存同时过期,导致大量的请求直接查询数据库,从而对数据库造成了巨大的压力,严重情况下可能会导致数据库宕机的情况叫做缓存雪崩。
我们先来看下正常情况下和缓存雪崩时程序的执行流程图,正常情况下系统的执行流程如下图所示:
缓存雪崩的执行流程如下:
从以上图片我们可以发现,1导致缓存雪崩的主要原因有以下三个:
- 缓存过期时间设置不合理 :由于大量缓存数据设置的过期时间相同,导致在某一时刻缓存大量失效,这样就使大量请求直接打到数据库上。
- 提供缓存的服务器发生障碍:缓存的服务器出现故障,无法提供缓存服务,那么所有请求就会直接访问数据库。
- 缓存数据的热点分布不均:由于是大量缓存直击数据库,所以可能是热点数据分布不均匀,都集中到某个缓存节点上,当这些节点发生故障或者数据失效的时候,会导致请求直接打到数据库。
1.1 解决缓存雪崩问题
① 随机生成缓存过期时间
可以避免缓存同时过期
package org.example;import redis.clients.jedis.Jedis;import java.util.Random;public class Main {public static void main(String[] args) {// 连接到本地 Redis 服务Jedis jedis = new Jedis("localhost", 6379);//缓存原来的失效时间int exTime = 10 * 60;//随机数生成Random randow = new Random();jedis.setex("myKey", exTime+ randow.nextInt(1000), "Hello, Redis!");// 关闭连接jedis.close();}
}
② 使用多级缓存(成本高,但是也较为主流)
二级缓存指的是除了Redis缓存之外,再设计一个二级缓存,这个二级缓存的过期时间比Redsi中要大一点,当Redis是失效后,先查二级缓存,如果查到数据了,就会直接从二级缓存拿数据返回给前端。不会走数据库,毕竟数据库的资源很宝贵。
这里的本地缓存:可以是mybatis的二级缓存(后两者更为主流,因为mybatis的二级缓存可以存的东西太少了),或者是Google的Guava Cache,Caffeine等。
但是设计二级缓存需要多写很多代码,而且会增加系统的复杂性。虽然查询的时候,走二级缓存没有问题,但是应用程序执行写入操作的时候,那么原本只需要保证Redis里的数据库和数据库里的数据一致即可,但是现在还要保证二级缓存的一致性,数据的一致性更难保证了。
但是Caffeine有方案可以保证本地缓存一致性的问题。
③ 缓存过期前预加载:
在缓存即将过期之前,提前异步加载缓存,避免在缓存失效时大量请求直接打到数据库或者后端服务。
例如看门狗机制,但是实现起来并不简单,因为还需要设置定时任务之类,但是定时任务也有可能会挂,并且也是有一定开销。
④ 开启限流或降级功能:
当缓存发生雪崩时,采用限流或降级的机制来减少服务器的压力,保证系统的可用性。
⑤ 实时监控和预警:
通过监控缓存的状态和命中率,及时发现缓存问题,预警系统管理员或运维人员。
二、缓存穿透
缓存穿透是指查询数据库和缓存都无数据,因为数据库查询无数据,出于容错考虑,不会将结果保存到缓存中,因此每次请求都会去查询数据库,从而给数据库带来了额外的压力,降低了系统性能的情况就叫做缓存穿透。
也就是说缓存穿透是因为数据库查询无数据,出于容错考虑,不会将结果保存到缓存中,因此每次请求都会去查询数据库,这种情况就叫做缓存穿透。
缓存穿透 执行流程如下图所示:
缓存穿透执行流程:Redis 和 数据库 都被穿透。
2.1 解决缓存穿透
缓存穿透的常见解决方案有以下几个:
1.缓存空对象: 对于查询结果为 nul 或不存在的数据,也可以将它们以特殊值(如"NULL"、特定标识符)进行缓存,并设置较短的过期时间。这样,短时间内相同的查询请求就可以直接从缓存中获得响应,避免了对数据库的直接查询。
2. 布隆过滤器(Bloom Filter): 在请求到达缓存之前,先通过布降过滤器判断数据可能存在还是一定不存在。对于确定不存在的数据,可以直接返回;可能存在则继续査询缓存和数据库。布隆过滤器是一种空间效率极高的概率型数据结构,它会给出“可能存在“或“肯定不存在”的答案。
3. 开启限流功能:当发现大量连接未命中的请求时,可以采用限流策略限制同一时间段内向数据库发送的查询请求数量,减轻数据库压力。
2.2 什么是布隆过滤器?
布隆过滤器是一种空间效率极高的概率性数据结构,可以用于判断一个元素是否在一个集合中。
它基于位数组和多个哈希函数的原理,可以高效地进行元素的查询,并且占用的空间相对较小,
如下图所示:
这里面存的就是比特,即0或1。根据 key 值计算出它的存储位置,然后将此位置标识全部标识为 1(未存放数据的位置全部为 0),查询时也是查询对应的位置是否全部为 1,如果全部为 1,则说明数据是可能存在的,否则一定不存在.
这种采用存储三个比特的方法,可以有效避免hash冲突,因为如果只存储一个的话,hash冲突可能发生比较频繁。
也就是说,如果布隆过滤器说一个元素不在集合中,那么它一定不在这个集合中;但如果它说一个元素在集合中则有可能是不存在的(存在误差)。
执行过程
布隆过滤器的具体执行步骤如下:
- 在 Redis 中创建一个位数组,用于存储布降过滤器的位向量,每个位置的值设置为 0.
- 添加元素到布隆过滤器时,对元素进行多次哈希计算,并将对应的位数组位置设置为 1。
- 查询元素是否存在时,对元素进行多次哈希计算,并检查对应的位数组位置是否都为 1,都为1 表示可能存在,其中有一个为 0则一定不存在。
使用场景
布隆过滤器的主要使用场景有以下几个:
- 大数据量去重:可以用布降过滤器来进行数据去重,判断一个数据是否已经存在,避免重复插入。
- 防止缓存穿透:可以用布隆过滤器来过滤掉恶意请求或请求不存在的数据,避免对后端存储的频繁访问。
- 网络爬虫 URL 去重:可以用布降过滤器来判断 URL 是否已经被爬取,避免重复爬取。
三、缓存击穿
缓存击穿指的是某个热点缓存,在某一时刻恰好失效了,然后此时刚好有大量的并发请求,此时这些请求将会给数据库造成巨大的压力,这种情况就叫做缓存击穿。缓存击穿的执行流程如下图所示:
主要原因是: 热点数据再缓存中失效或淘汰,并发请求同时访问该数据库,导致缓存无法命中。
缓存击穿的执行流程:(Redis 被击穿)
3.1 解决缓存击穿
① 设置永不过期:
对于某些热点缓存,我们可以设置永不过期,这样就能保证缓存的稳定性,但需要注意在数据更改之后,要及时更新此热点缓存,不然就会造成查询结果的误差。
② 缓存过期前预加载
在缓存即将过期之前,提前异步加载缓存,避免在缓存失效时大量请求直接打到数据库或者后端服务。
例如看门狗机制,但是实现起来并不简单,因为还需要设置定时任务之类,但是定时任务也有可能会挂,并且也是有一定开销。
③ 使用多级缓存:
可以使用多级缓存架构,将热门数据同时缓存在多个缓存节点上,避免单一节点故障导致请求直接访问数据库或者后端服务,例如可以设计多级缓存,也就是使用分布式缓存(Redis)+本地缓存(Caffeine/Guava Cache),如下图所示:
但是设计二级缓存需要多写很多代码,而且会增加系统的复杂性。虽然查询的时候,走二级缓存没有问题,但是应用程序执行写入操作的时候,那么原本只需要保证Redis里的数据库和数据库里的数据一致即可,但是现在还要保证二级缓存的一致性,数据的一致性更难保证了。
但是Caffeine有方案可以保证本地缓存一致性的问题。
④ 开启限流或降级功能:
当缓存发送雪崩时,采用限流或降级的机制来减少服务器的压力,保证系统的可用性。
3.2 如何保证数据一致性问题?
如何保证本地缓存的一致性?
在分布式系统中,使用本地缓存最大的问题就是一致性问题,所谓的一致性问题指的是当数据库发生数据变更时缓存也要跟着一起变更。而分布式系统中每台机器都有自己的本地缓存,所以想要保证(本地缓存的)一致性是个比较难的问题,但通过以下手段可以最大程度的保证本地缓存的一致性问题:
- 本地缓存失效时间尽量短,短的存活周期,保证了尽可能的保证了一致性。
- 通过微服务中的配置中心(例如 Nacos)来协调,因为所有服务器都连接的配置中心,所以当数据修改之后可以给配置中心推送一个配置修改的信息,然后配置中心再把变更信息推送给各个服务,服务订阅到配置变更消息之后,就会更新自己的本地缓存,这样就实现了数据的一致性。
- 使用缓存框架的自动更新功能,例如 Caffeine 中的 refresh 功能自动刷新缓存。
不同的业务系统,会采用不同的解决方案,例如以下这些场景和对应的解决方案:
- 如果对数据一致性要求不是很高,并且程序的并发压力不大的情况下,可能使用方案 1,也就是设置本地缓存短时间内失效的解决方案,因为它的实现最简单。
- 如果对数据的一致性要求极高,且有配置中心的情况下,可使用配置中心协调和同步本地缓存。
- 相反,如果对一致性要求没有那么高,且为高并发的系统,那么可以采用本地缓存的自动更新功能来实现。