日常开发中,redis作为缓存层使用,在面对特定的场景下简单的使用或者在写代码的时候没有结合对应的业务进行思考,容易带来一些性能问题甚至导致业务异常而导致服务停摆,无法提供正常的服务。所以在使用缓存时也要多进行一些思考,提升接口性能的同时也要尽量去规避一些潜在的风险。常见的预防缓存穿透、缓存雪崩、缓存击穿手段,结合业务量去做一些改进。
1、缓存穿透:
1.1、解释:
一般我们查询时,先查询缓存再查询数据库,缓存命中则直接返回,不命中则去查询数据库。那么在查询数据库的时候,结果有两种,一个是有数据,则放入缓存同时返回;另一个是数据库里也没有对应的数据,那么此时缓存和数据库中都没有对应的数据。如果是第二种情况的查询,这样的查询数据足够大,比如恶意开启十万个线程去一直查询,那么势必会给数据库带来相当大的压力,那么这种情况就称之为缓存穿透。简单来说,缓存穿透就是大量请求去查询一个缓存和数据库中都没有的数据。
1.2、对应措施:
思考:缓存穿透带来的问题是什么,是给数据库带来了巨大压力,那么解决的角度就是如何避免给数据库带来压力。将查询给到redis就能缓解数据库的压力。
1.2.1、缓存空值:
所谓缓存空值,就是当查询数据库里也不存在时,就往缓存里放一个空值,比如空字符串"",那么当请求到达时,先判断缓存里是不是null,是null的话,就去数据库里查询,数据库里也不存在,就存一个""进去,那么当下一个请求到达时,判断如果是"",则返回不存在,如果不是,返回对应数据即可。这样就极大降低了数据库的压力。
思考: 缓存空值会带来那些问题。第一,如果缓存了大量的空值,会占用额外的内存。第二,如果有更新操作向数据库里添加了对应的记录,但是没有删除缓存,那么就会造成缓存和数据库里数据不一致,在不触发redis的缓存淘汰机制的前提下,可以认为是永不过期的,那么查询一直得到的都是数据不存在。所以在设置空值""的时候要加一个TTL(Time To Live)来兜底,等缓存失效了自然会去数据库里查询到最新的。注意,这样也会存在部分用户在某段时间看到的还是不存在,合理设置TTL,业务上也能容忍即可。
1.2.2、布隆过滤器:
请求到达,在查询缓存之前,先经过布隆过滤器,它会通过自己的哈希算法来表示存在的数据,这样当请求到达时,数据存在,则走后续的查询缓存查询数据库的流程,若数据不存在,则直接返回不存在,从而降低数据库的压力。
注意:
布隆过滤器表示不存在的数据,则一定不存在,表示存在的数据却不一定存在,毕竟有哈希冲突的可能。所以如果布隆过滤器表示存在,而实际不存在,那么还会带来缓存穿透的问题。且实现维护起来比缓存空值""的成本高,慎用。
1.2.3、结论:
一般业务来讲,从代码实现、成本以及收益的角度来讲,缓存空值""并加一个TTL来兜底的方案是一个最优解。其它方案有类似限流降级等等,比较复杂。
2、缓存雪崩:
2.1、解释:
缓存雪崩就是说有大量的key同时失效,然后此时大量相关请求同时到达,压力都会给到数据库。比如说大量缓存数据的TTL都设置成了1分钟,到点失效,此时大量用户的相关请求到达,由于缓存中数据不存在,所以只能去数据库中查询,就会给数据库带来压力。另外就是如果redis服务宕机不可用了,那么请求也会都打到数据库里,这种雪崩是最可怕的。
2.2、对应措施:
2.2.1、TTL增加随机值:
非常简单的一种做法,既然是大量的key同时失效,而恰巧又赶上相关的查询请求同时到达,导致缓存雪崩,那么给TTL增加一个随机值,比如TTL是1分钟,再加一个随机值0到1000毫秒,那么就会很大程度上预防发生缓存雪崩。TTL与随机值结合业务合理设置。
2.2.2、部署redis集群,做高可用:
利用redis的哨兵机制,当一台服务挂掉,会选举另一台提供服务,提高可用性。
2.2.3、做好接口限流降级:
当QPS将超过服务的能力范围的时候,限流或者直接拒绝提供服务。
2.2.4、设置多级缓存:
一层缓存不够,那就多加几层,代价较高,慎用。
2.2.5、结论:
结合业务,TTL+随机值就能搞定的就不用其它,越简单越好。
3、缓存击穿:
3.1、解释:
缓存击穿就是并发条件下,大量请求同时请求一个缓存里不存在但是数据库里存在的数据,且这样的缓存数据重建起来可能还比较耗时。由于没有命中缓存,这些请求就会打到数据库上,瞬时给数据库带来极大压力,QPS高的话搞崩数据库也是有可能的。这种问题一般也叫热点Key问题,访问频率很高。
3.2、对应措施:
3.2.1、分布式锁:
使用互斥锁,当大量查询请求并发查询,只允许一个线程去查询,然后将数据塞回缓存。当某一个线程持有锁,其它线程等待一个时间比如200毫秒,再次发起查询,只有获取锁的线程可以去数据库查询数据并更新缓存,那么当缓存更新好了之后,其它线程自然能够从缓存中获取数据,这样,就避免给数据库带来压力。比如说使用redis的setnx去作为互斥锁达到这样的效果,为锁添加一个过期时间防止意外无法释放锁。
注意: 查询一进来发现缓存里没有值,那么尝试获取锁,当获取锁成功以后要再次检查一下缓存里有没有值,做一个double check,有值就直接返回。原因就是不做double check的话,因为是并发条件下,获取锁操作是在第一次缓存未命中的情况下发生的,假如此时获取锁的线程是线程A,如果恰好有一个线程B查询缓存未命中,此时又恰好线程A更新完缓存释放了锁,就会导致线程B获取锁,去查询数据库又更新缓存,这就导致重复更新,浪费资源。
3.2.2、设置热点数据永不过期(逻辑上):
带来缓存击穿问题的一般就是热点数据,热点数据就是经常查询但一般不咋更新的数据,比如说做活动时相关的热点数据,可能来自于好多个服务,组织这些数据也比较耗时,一般也会做缓存预热。此时我们可以将这些数据存入缓存中时不设置TTL,表示永不过期,在查询时如果缓存未命中则返回空即可,因为我们做了预热的那就认为缓存中一定存在的。但是这样也会有问题,如果此时数据库数据有更新,但是由于设置了永不过期,那么就会一直查询到旧的数据。所以我们选择在存入缓存的时候添加一个字段,叫逻辑过期时间。当请求到达时判断一下是不是已经过了设置的逻辑过期时间,如果是,那么就去数据库里查询,要注意,如果查询比较耗时,那么可以开一个线程去查询,然后先将旧的数据返回,等新的数据组织好放入缓存,那么之后查到的就是新的了。另外,这个查询数据库的操作要使用分布式锁去实现,只让一个线程去查询,其余获取不到锁的直接返回旧数据。简单的分布式锁可以使用setnx去实现,做double check,注意增加一个过期时间,防止意外导致无法释放锁。
3.2.3、结论:
没这么大的业务量,没经历过,了解一下...