缓存穿透
原因
查询一个数据库不存在的数据,既然数据库不存在,那么缓存自然也不会存在,因此所有这种垃圾查询都会打到数据库上,给数据库造成负担。
解决方法
- 布隆过滤器
- 缓存空对象
布隆过滤器
相信不少同学面试时被问过场景设计题:如果目前我们有个营销垃圾邮箱的汇总表,我们希望设计一个高效的拦截过滤器,怎么设计呢?
大家肯定脱口而出:hashmap/hashset。
对,这个思路一点没错,如果我们的邮箱汇总表不大,当然可以这么干。但如果汇总表稍微大一点点比如上到10亿,那就不能这么干了。一般来说面试官会回答:这么做占用空间太大,服务器内存撑不住,可以换一个思路,我们的这个业务可以允许一定的误报。
既然面试官都给提示了,那咱就往挤压内存占用的思路去考虑,既然又希望减少空间,又能接受误报,可以考虑这个思路。
首先我们先设置一个空bit数组,初始全0:
设计k个hash映射函数,这k个映射函数各不相同,然后开启一个for循环,把在汇总表中的邮箱通过这个映射函数得到数组指定位置,并置为1。比如这样:
然后这样
下次用户有想查询的邮箱,可以通过这一系列hash函数,判断对应位置是否全为1。如果是,则很大概率就是垃圾邮箱了。为啥说是很大概率,因为完全不同的字段,在一个hash函数中,映射也会相同的。
那如何减少误判概率?
- 增加hash函数个数
- 增加这个数组长度
最后提一下,这个数组可以就认为是布隆过滤器。布隆过滤器详情
缓存空对象
这个就很好理解了,一般查询的逻辑,是这样的:
- 用户发送查询请求
- 缓存层收到请求,在缓存中查找,如果有则返回,没有则走步骤3
- 数据库收到查询请求,如果存在就返回结果,并放到缓存,不存在直接返回
举例来说,我们用了个员工id和其对应的工资表。如果员工有100个人,那我们id肯定是1-100排列。如果此时有个请求,想查询id=1000的,那肯定啥也查不到,会被直接返回。
而缓存空对象,做的就是把这个值也缓存下来,即在缓存层中,添加一个id为1000,值为null的键值对。下次有对id为1000的请求,查询直接打到缓存上,减少了数据库压力。
但这种操作会增加内存开销,所以如果采用这种方法,一般空对象缓存的过期时间极短
缓存击穿
原因
我们知道,缓存层会设置一个数据过期时间,防止数据过多时,内存增大,撑爆服务器。
因此,当某一时刻,一个热点数据过期时,此时若有大量查询该数据的请求时,由于数据过期,缓存中没有,即“击穿了缓存”,所有的请求都会打到数据库,造成数据库负担。
解决方法
- 从Mysql角度出发
咱站在MySQL的角度,多心疼一下它。不就是大流量我MySQL处理不了嘛,那我直接把流量想个招,变成原来的1/100-1/10,那MySQL肯定轻轻松松可以处理。
那咋搞呢?加锁,线程读数据,之前100个可以同时读取数据,现在加锁,线程读数据,先看看数据有没有上锁,上锁了,等着,没上锁,获取锁后,再给数据库请求。数据库压力肯定瞬间下来了。
但有的读者肯定会问:这样子,用户体验是不是就会卡顿。这是必然的,牺牲用户体验,保证数据库不死机,这就是这个方式的设计出发点。
- 从Redis角度出发
咱站在Redis角度想想。为啥出现目前问题,还不是因为Redis缓存数据过期没了。那如果对Redis动刀,怎么解决?那肯定是修改Redis缓存的数据过期策略了。最常见的,也是大家八股文背的最多的,就是设置热点数据永不过期。
但这个方法,咋说呢,简单粗暴,很多情况下,我们无法预知热点数据。比如换做半年前,鸿星尔克出圈前,电商平台设计者在初始化redis数据库的时候,也不可能直接把它归为热点数据。
那咋搞呢?还是从数据过期策略出发,咱可以搞个这样的逻辑嘛。
如果发现这个数据快过期,并且好像最近这个数据访问特别多,后台新启一个异步线程,重新在缓存层中添加这个数据的缓存,给这个数据再续一续命。
缓存雪崩
原因
在某一时刻,一大批缓存同时失效,此时若有大量对于这一批数据的请求进来,所有的请求都会打到数据库上,造成数据库崩溃。
缓存雪崩这个名词非常形象,雪崩这两个词完美的比喻了当前的情况。之前提到的缓存击穿,是单点情况,雪崩就是多点情况。
解决方法
- 从Mysql的角度出发
无论是对很多键的并发查询(缓存雪崩),还是一次性对同一个键的很多次并发查询(缓存击穿),MySQL的解决方法都是简单粗暴:强行减少请求量。
和击穿一样,加锁。线程读数据,先看看数据有没有上锁,上锁了,等着,没上锁,获取锁后,再给数据库请求。数据库压力肯定瞬间下来了。
这个解决方法带来的问题也很好回答:用户体验是不是就会卡顿。这是必然的,牺牲用户体验,保证数据库不死机,这就是这个方式的设计出发点。
但相对缓存击穿的情形,对于缓存雪崩采取这种策略,效果并不会这么给力。原因也很简单,此时很多客户端的访问请求,不单单是基于一个键,并发量的减少不会像缓存击穿这么明显。
- 从Redis角度出发
-
设置热点数据永不过期
-
设置随机过期时间,避免缓存在同一时间大批失效。换句话,就是让缓存失效时间均匀一点。
如果我们一开始就没缓存怎么办?
在项目发布伊始,数据库中存满了数据,缓存却是空的。如果此时系统上线,请求又多,那就是妥妥的雪崩。甚至可以说,雪崩是这种情况下的必然事件。
对于这种情况下怎么处理呢?
这里得引入一个名词:灰度发布。上了班的老铁们应该不陌生,但由于可能有读者朋友还在上学,我们普及一下:
灰度发布
首先灰这个词是怎么来的呢?当然是由黑和白来的,互联网一般会把新系统完全上线或者新系统没上线定义为黑和白。所以新产品在处于没完全上线,就被定义为灰了。我们经常听说的A/B测试就是一种灰度发布方式,即一部分用户继续用老系统A,另一部分用户用新系统B,如果用户对新系统意见不大,甚至反馈比A好,那就逐步扩大范围,慢慢把所有用户都迁移到B上来。
聊完灰度发布,相信大家瞬间对刚刚问题有了答案。如果采用灰度发布方式,进行缓存预热,一开始流量小,数据库hold住,缓存也可以多出很多数据,渐渐增加流量,缓存也不为空,就轻轻松松解决这个问题了。
缓存雪崩处理原则
了解了这么多,来看个面试题吧。
面试官:如果某个查询逻辑确实复杂,数据库回应很慢,redis又对这条查询没缓存的情况,这种情况引起雪崩,我们怎么处理?
此时大家一定牢记一条原则:
结合业务需求具体分析!
结合业务需求具体分析!
结合业务需求具体分析!
来解释一下面试题:用户A发了个非常复杂的查询请求,我们后端数据库收到查询后,开始执行查询,由于查询可能涉及多表联查,花的时间不少,还在查询过程中, 此时用户B,C,D,E,F…发了同样复杂的请求过来,由于缓存此时为空,请求毫无疑问,就打到了数据库上,要是服务器性能差点,数据库就雪崩了。此时咱们外层看着好像Mysql收到的请求好像也不多啊…
遇到这种情况,一定具体分析业务大体需求了。
- 如果注重时效性,那就得一开始就对这些复杂查询做做缓存。
- 不注重,那就上锁,加锁,超时的请求返回错误信息等等,减少数据库压力。