1.缓存穿透
1.什么是缓存穿透?
缓存穿透简单说就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据造成了巨大的压力,可能直接就被这么多请求搞宕机了。
缓存穿透
举个栗子:某黑客恶意制造一些非法的 key 发起大量请求,导致大量请求落到数据库,结果是没有查到对应的数据。也就是最终请求都落到了数据库上,造成了巨大的压力。
2.有哪些解决办法?
- 1)缓存无效 key: 如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去设置过期时间,具体命令如下:SET key value EX 10086。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key。很明显这种方案不能从根本上解决问题。一般设计key是:表名:列名:主键名:主键值。 Java举例:
public Object getObjectInclNullById(Integer id) {
// 从缓存中获取数据
Object cacheValue = cache.get(id);
// 缓存为空
if (cacheValue == null) {
// 从数据库中获取
Object storageValue = storage.get(key);
// 缓存空对象
cache.set(key, storageValue);
// 如果存储数据为空,需要设置一个过期时间(300秒)
if (storageValue == null) {
// 必须设置过期时间,否则有被攻击的风险
cache.expire(key, 60 * 5);
}
return storageValue;
}
return cacheValue;
}
- 2)布隆过滤器 布隆过滤器是一个非常神奇的数据结构,可以非常方便的判断一个给的的数据是否存在于海量数据中。可以看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的 List、Map、Set等数据结构,它占用的空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的,理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
Bloom Filter 的简单原理示意图
Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数组中的每个元素都只占用 1 bit,并且每个元素只能是 0 或者 1(代表 false 或者 true),这也是 Bloom Filter 节省内存的核心所在。这样来算的话,申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024KB ≈ 122KB的空间。
位数组
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
加入布隆过滤器后的缓存处理流程图如下:
加入布隆过滤器后的缓存处理流程图
- 3)接口限流
根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,将异常 IP 加入黑名单。
2.缓存击穿
什么是缓存击穿?
缓存击穿中,请求的 key 对应的是 热点数据,该数据 存在于数据库中,但不存在于缓存中(同城是因为缓存中的那份数据已经过期)。这就可能导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
缓存击穿
举个栗子:秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。
有哪些解决办法?
- 设置热点数据永不过期或者过期时间比较长。
- 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
- 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。
缓存穿透与缓存击穿有什么区别?
缓存穿透中,请求的 key 即不存在与缓存中,也不存在于数据库中。
缓存击穿中,请求的 key 对应的是 热点数据,该数据 存在于数据库中,但不存在于缓存中(通常是缓存中的那份数据已经过期)。
3.缓存雪崩
什么是缓存雪崩?
缓存雪崩描述的是这样一个场景:**缓存在同一时间大面积的失效,导致大量的请求直接落到了数据库上,对数据库造成了巨大的压力。**就好像雪崩一样,摧枯拉朽之势将压力都堆在了数据库上。
另外,缓存服务宕机也会导致缓存雪崩现象,导致所有的请求落到了数据库上。
缓存雪崩
举个栗子:数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。
有哪些解决方法?
针对 Redis 服务不可用的情况:
- 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
- 限流,便面同时处理大量的请求。
- 多级缓存,例如本地缓存+Redis 缓存的组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
针对热点缓存失效的情况:
- 设置不同的失效时间比如随机设置缓存的失效时间。
- 缓存永不失效(不推荐,不实用)。
- 缓存预热,也就是在程序启动后或运行过程中,主动将热点数据加载到缓存中。
缓存预热如何实现?
常见的缓存预热方法有两种:
- 使用定时任务,比如 xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。
- 使用消息队列,比如 Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费队列中的数据,根据主键或者 ID 查询数据库并进行缓存。
缓存雪崩和缓存击穿有什么区别?
缓存雪崩和缓存击穿比较类似,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点不存在于缓存中(通常是因为缓存中的那份数据已经过期)。