缓存穿透、缓存击穿、缓存雪崩、热点数据失效的解决方法

我们使用缓存时,一般流程是这样的:当需要查询一条数据,先去查询缓存,如果缓存有就直接返回,如果没有就去查询数据库,然后返回。这时是有可能会出现一些异常现象的。

一、缓存穿透

什么是缓存穿透

正常情况下,我们去查询数据都是存在于缓存中,因为使用缓存目的就是尽量少的查询数据库,偶尔可能会有数据存在数据库里面的情况。

如果有一个请求去查询一条数据库中根本就不存在的数据,也就是缓存和数据库都查询不到这条数据,就会导致请求每次都会穿透缓存,打到数据库。

这种查询不存在数据的现象我们称为缓存穿透。

带来的问题

如果恶意的人拿一个不存在的数据来不停地查询,每次缓存都未命中,然后请求打到数据库,这样数据库很有可能扛不住压力而挂掉。

解决办法

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

  1. 缓存无效key

    之所以会发生穿透,就是因为缓存中没有存储这些空数据的key。从而导致每次查询都到数据库去了。那么我们就可以为这些 key 对应的值专门设置一个 null 值丢到缓存里面去,同时设置一个较短的过期时间。后面再出现查询这个key 的请求的时候,直接返回 null。

    这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

  2. BloomFilter(布隆过滤器)

    BloomFilter 类似于一个hbase set 用来判断某个元素(key)是否存在于某个集合中。将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。

    在查询的时候先去 BloomFilter 去查询 key 是否存在,如果不存在就直接返回,存在再走查缓存 -> 查 DB。

    但是需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

如何选择

针对于一些恶意攻击,攻击带过来的大量key 是不存在的,那么我们采用第一种方案就会缓存大量不存在key的数据。

此时我们采用第一种方案就不合适了,我们完全可以先对使用第二种方案进行过滤掉这些key。

针对这种key异常多、请求重复率比较低的数据,我们就没有必要进行缓存,使用第二种方案直接过滤掉。

而对于空数据的key有限的,重复率比较高的,我们则可以采用第一种方式进行缓存。

二、缓存击穿

什么是缓存击穿

在平常高并发的系统中,大量的请求同时查询一个 key 时,此时 key 正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们称为缓存击穿。

会带来什么问题

会造成某一时刻数据库请求量过大,压力剧增。

如何解决

产生原因是多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。

其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。

三、缓存雪崩

什么是缓存雪崩

缓存雪崩的情况是说,当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,会有大量的请求进来直接打到 DB 上面。结果就是 DB 撑不住,挂掉。

解决办法

事前:

使用集群缓存,保证缓存服务的高可用

这种方案就是在发生雪崩前对缓存集群实现高可用,如果是使用 Redis,可以使用 主从+哨兵 ,Redis Cluster 来避免 Redis 全盘崩溃的情况。

事中:

ehcache本地缓存 + Hystrix限流&降级,避免MySQL被打死

使用 ehcache 本地缓存的目的也是考虑在 Redis Cluster 完全不可用的时候,ehcache 本地缓存还能够支撑一阵。

使用 Hystrix 进行限流 & 降级 ,比如一秒来了 5000 个请求,我们可以设置假设只能有一秒 2000 个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑。

然后去调用我们自己开发的降级组件(降级),比如设置的一些默认值呀之类的。以此来保护最后的 MySQL 不会被大量的请求给打死。

事后:

开启 Redis 持久化机制,尽快恢复缓存集群

一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。

四、热点数据集中失效问题

这个问题类似于二中的缓存击穿。我们在设置缓存的时候,一般会给缓存设置一个失效时间,过了这个时间,缓存就失效了。

对于一些热点的数据来说,当缓存失效以后会存在大量的请求过来,然后打到数据库去,从而可能导致数据库崩溃的情况。

解决办法

  • 设置不同的失效时间

    为了避免这些热点的数据集中失效,那么我们在设置缓存过期时间的时候,我们让他们失效的时间错开。

    比如在一个基础的过期时间上加上或者减去一个范围内的随机值。

  • 互斥锁

    结合上面的击穿的情况,在第一个请求去查询数据库的时候对他加一个互斥锁,其余的查询请求都会被阻塞住,直到锁被释放,从而保护数据库。

    但是也是由于它会阻塞其他的线程,此时系统吞吐量会下降。需要结合实际的业务去考虑是否要这么做。

结尾

参考https://blog.csdn.net/zeb_perfect/article/details/54135506#commentBox