4. 缓存异常[❤️]
缓存异常有四种类型,分别是 缓存穿透、缓存雪崩、缓存击穿、缓存和数据库的数据不一致。
4.1 缓存穿透
4.1.1 介绍
先来看一个常见的缓存使用方式:读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。
缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。
通俗点说,读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库,这就是缓存穿透。
缓存穿透一般都是这几种情况产生的:
- 业务不合理的设计,比如大多数用户都没开守护,但是你的每个请求都去缓存,查询某个userid查询有没有守护。
- 业务/运维/开发失误的操作,比如缓存和数据库的数据都被误删除了。
- 黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在的业务数据。
4.1.2 解决方案
- 将无效的key存放进Redis中 当出现 Redis 查不到数据,数据库也查不到数据的情况,我们就把这个 key 保存到 Redis 中,设置 value="null",并设置其过期时间极短,后面再出现查询这个 key 的请求的时候,直接返回 null,就不需要再查询数据库了。 但这种处理方式是有问题的,假如传进来的这个不存在的 Key 值每次都是随机的,那存进 Redis 也没有意义。
- 使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。
布隆过滤器原理: 它由初始值为0的位图数组和N个哈希函数组成。 对一个key进行N个hash算法获取N个值,在比特数组中将这N个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该key存在。 如果有不为1 的,那么这个key一定不存在,如果都为1,那么这个key可能存在。
如何选择这两种方案呢?
针对一些恶意攻击,攻击带过来的大量 key 是随机,如果我们采用第一种方案就会缓存大量不存在 key 的数据,所以这种方案就不合适了,我们可以先对使用布隆过滤器方案进行过滤掉这些 key。所以,针对这种 key 异常多、请求重复率比较低的数据,优先使用第二种方案直接过滤掉。
而对于空数据的 key 有限的,重复率比较高的,则可优先采用第一种方式进行缓存。
4.2 缓存雪崩
4.2.1 介绍
缓存雪崩: 指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至宕机。
造成雪崩的可能:
- 采用了相同的过期时间:一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值 一个较小的随机值,5小时 0到1800秒。
- Redis 宕机:Redis 故障宕机也可能引起缓存雪奔。这就需要构造Redis高可用集群啦。
4.2.2 解决方案
- 事前:
- 均匀过期:设置不同的过期时间,让缓存失效的时间尽量均匀,避免相同的过期时间导致缓存雪崩,造成大量数据库的访问。
如把每个 Key 的失效时间都加个随机值,
setRedis(Key,value,time Math.random() * 10000);
,保证数据不会在同一时间大面积失效。 - 分级缓存:第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。
- 热点数据缓存永远不过期。永不过期实际包含两层意思:
- 物理不过期,针对热点 key 不设置过期时间
- 逻辑过期,把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建
- 保证 Redis 缓存的高可用,防止 Redis 宕机导致缓存雪崩的问题。可以使用 主从 哨兵,Redis 集群来避免 Redis 全盘崩溃的情况。
- 均匀过期:设置不同的过期时间,让缓存失效的时间尽量均匀,避免相同的过期时间导致缓存雪崩,造成大量数据库的访问。
如把每个 Key 的失效时间都加个随机值,
- 事中:
- 互斥锁:在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量,比如某个 key 只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降
- 使用熔断机制,限流降级。当流量达到一定的阈值,直接返回 “系统拥挤” 之类的提示,防止过多的请求打在数据库上将数据库击垮,至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。
- 事后: 开启 Redis 持久化机制,尽快恢复缓存数据,一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。
4.3 缓存击穿
4.3.1 介绍
缓存击穿: 指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到数据库。
与缓存雪崩的区别:
缓存雪奔是指数据库压力过大甚至宕机,缓存击穿只是大量并发请求到了DB数据库层面。可以认为击穿是缓存雪奔的一个子集吧。有些文章认为它俩区别在于击穿是针对某一热点key缓存,雪奔则是很多key。
4.3.2 解决方案
从两个方面解决,第一是否可以考虑热点 key 不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。
解决方案就有两种:
- 使用互斥锁方案:缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
- “热点数据永不过期”,永不过期实际包含两层意思
- 物理不过期,针对热点 key 不设置过期时间
- 逻辑过期,把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建
我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表