缓存雪崩、缓存穿透、缓存击穿、缓存预热、缓存更新、缓存降级梳理
一、缓存使用流程
时间 | 事件 |
---|---|
T1 | 客户端发起请求 |
T2 | 后台接收请求,先去缓存中取数据,如果取到数据直接返回数据,请求结束,如果没有取到数据,执行T3 |
T3 | 从数据库中取数据,如果取到数据更新缓存,并返回数据,如果数据库中也没有取到数据,那直接返回空结果 注:如果数据库中没有取到数据,也建议在缓存中存储一个默认空值,这样可以避免用户一直请求去查询数据库 |
以上是缓存正常使用的一个请求流程,而缓存雪崩、缓存穿透、缓存击穿则是在使用缓存过程中存在的一些情况,下面将详细分析一下
二、缓存雪崩
场景:同一时间大面积的缓存数据过期(或缓存服务器挂了),此时又有大批量的请求这些数据,缓存中取不到数据,就会将请求全部转发到数据库,数据库瞬时压力过重雪崩。例一大批商品抢购,同一时间下架,将商品数据从缓存中删除,同时大批用户去访问这些商品,缓存中没有数据,所有的请求都会去查询数据库
解决方式:
1、尽量错开过期时间,不要大批量设置同一过期时间,如热数据和冷数据设置不同的过期时间,不同类型的数据设置不同的过期时间
2、一般并发量不是特别多的时候,使用最多的解决方案是加锁排队
3、给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存(实际缓存数据的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存)
三、缓存穿透
场景:指查询数据库中一定不存在的数据,缓存和数据库中都没有这个数据,以致于产生两条无用查询,每次请求都会到达数据库,就失去了使用缓存的意义,在流量大的时候,数据库可能会挂掉,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞
解决方式:
1、代码中如果可以明确判断一些值,则请求数据之前进行验证拦截,如商品编号只会在1-100,如果客户端请求编号为-1,则直接进行拦截返回,避免无效请求到达缓存和数据库
2、如果查询结果为空,我们设置一个默认值进行缓存,过期时间设置短一些,最长不超过五分钟,这样下次再请求的时候可以直接从缓存中返回数据,不用再去访问数据库,这种方式最简单粗暴
三、缓存击穿
场景:指缓存中没有但数据库中有数据,一般是因为缓存数据时间到期了,如果这时由于并发用户特别多,同时大量请求读缓存没读到数据,又同时去数据库取数据,引起数据库压力瞬间增大,造成过大压力
解决方式:
1、使用互斥锁(mutex key)
public String get(key) {
String value = redis.get(key);
if (value == null) { //缓存中未取到值,表示缓存过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
2、热点数据设置永远不过期(第一种不设置过期时间,第二种设置过期时间,过期时间快到的时候异步延长过期时间)
缓存雪崩和缓存击穿、缓存穿透和缓存击穿的区别:
缓存雪崩:缓存雪崩是不同数据都过期了很多数据都查不到从而查数据库
缓存击穿:缓存击穿指并发查同一条数据
缓存穿透:缓存和数据库中都没有需要的值
缓存击穿:缓存中没有需要的数据,但数据库中有(击穿比较形象,击打后穿透,是有一个过程,说明是有缓存的;穿透就比较直接,相当于直接就过了,缓存没生效)
四、缓存预热
场景:系统上线时,同时将数据更新到缓存中,这样用户请求的时候,就可以直接访问缓存,不必去请求数据库
解决方式:
1、数据量不大的情况下,可以在系统启动的时候同时加载缓存
2、写一个刷新缓存的功能按钮或页面,特定的时候维护人员进行手动刷新
五、缓存更新
场景:除了redis设置过期时间的缓存数据清理,redis也提供了一些其他的更新淘汰策略,如缓存的数据量太大,达到了内存限制的最大值,将会出现redis内存不足的问题。redis允许用户通过配置maxmemory-policy 参数,指定redis在内存不足时的解决策略(详细可访问博客:https://blog.csdn.net/lemon_linaa/article/details/110200659)
noeviction:不删除策略,达到最大内存限制时,如果需要更多内存,直接返回错误信息。大多数写命令都会导致占用更多的内存(redis默认选项)
maxmemory-policy noeviction
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰(LRU算法)
使用场景:如果设置了过期时间,且分热数据与冷数据,推荐使用 volatile-lru 策略
其他:热数据(需要被计算节点频繁访问的在线类数据)、冷数据(是对于离线类不经常访问的数据,比如企业备份数据、业务与操作日志数据、话单与统计数据),可参考https://www.jianshu.com/p/053ba529bf02
maxmemory-policy volatile-lru
allkeys-lru:从所有数据集中挑选最近最少使用的数据淘汰(LRU算法)
使用场景:使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
值得一提的是,设置 expire 会消耗额外的内存,所以使用 allkeys-lru 策略,可以更高效地利用内存,因为这样就可以不再设置过期时间了
maxmemory-policy allkeys-lru
volatile-lfu:从已设置过期时间的数据集中挑选访问频率最少的数据淘汰(LFU算法)
maxmemory-policy volatile-lfu
allkeys-lfu:从所有数据集中挑选访问频率最少的数据淘汰(LRU算法)
maxmemory-policy allkeys-lfu
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
使用场景:很少使用
maxmemory-policy volatile-random
allkeys-random:从所有数据集中任意选择数据进行淘汰
maxmemory-policy allkeys-random
volatile-ttl:从已设置过期时间的数据集中优先删除剩余时间短的数据
使用场景:如果让 Redis 根据 TTL 来筛选需要删除的key,请使用 volatile-ttl 策略
maxmemory-policy volatile-ttl
六、缓存降级
场景:当并发量过高、服务出现问题(响应时间慢或不响应),为了使程序正常可用,需要对程序的使用进行降级处理,对一些非核心模块的业务进行请求访问降级,如支付宝的核心业务模块是转账,如果进行降级,可以选择对查看账单、蚂蚁森林、查看热销活动等非核心业务模块进行降级,降级的最终目的是保证核心服务可用,所以进行降级之前要对系统的业务模块进行梳理,理出哪些是可以降级的,哪些是无法降级的,根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务;根据服务范围:可以砍掉某个功能,也可以砍掉某些模块
限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量,已达到保护系统的目的,一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的,比如:延迟处理,拒绝处理,或者部分拒绝处理等等。
解决方式:
1、代码中核心业务模块进行峰值捕获,然后将非核心业务请求进行降级
2、编写降级页面,特定的时候维护人员进行手动降级