作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
学习必须往深处挖,挖的越深,基础越扎实!
阶段1、深入多线程
阶段2、深入多线程设计模式
阶段3、深入juc源码解析
码哥源码部分
码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】
码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】
码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】
码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】
打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】
1 介绍
在之前的一篇文章《一次缓存雪崩的灾难复盘》中,我们比较清晰的描述了缓存雪崩、穿透、击穿的各自特征和解决方案,想详细了解的可以移步。
最近在配合HR筛选候选人,作为大厂的业务方向负责人,招人主要也是我们自己团队在用,而缓存是必不可少的面试选项之一。下面我们就来聊一聊在特定业务场景下缓存击穿和雪崩的应对场景!
2 问题背景
- 一个核心的应用或者服务(比如微信、钉钉、百度APP),高峰QPS是百万甚至是千万
★ 分析:上述类型的应用具有很明显的峰值 高斯分布的特征,就是9~10点是用户早高峰。微信是,百度APP是,钉钉也是,钉钉一般给政企、教学等使用,通用是10点左右峰值期,每天的峰值如下:
- 应用缓存了用户的基本信息,如(姓名、性别、职业、地址等),假设以为用户Id为Cache的key,那每个用户都有一个基础信息的缓存。
- 因为不知名的原因,导致缓存都丢了(可能是缓存集体过期、故障导致缓存失效、程序bug导致缓存误删、服务器重启导致内存清理)。
- 恰巧是访问高峰期(比如9点早高峰),千百万的请求狂奔而来,查不到缓存,透过缓存层直接投入数据库。
- 基于磁盘的数据库的访问效率,性能,抗击打能力远逊于高速缓存,数据库很容易被打垮,造成服务雪崩。
4 候选人的各种答案(综合整理)
4.1 缓存预热
既然是可预见的峰值期,那么缓存预热是一个好办法,比如在9 ~ 10点是高峰期,在7 ~ 9点这两个小时中,可以均匀的把部分缓存做上。
缺点:这种仅仅只能解决可预见的缓存失效情况。如果是突发缓存失效情况,假设在10点高峰期因为某些原因(比如上面说的 故障导致缓存失效、程序bug导致缓存误删、服务器重启导致内存清理)是没有效果的。
4.2 非一致的过期时间
缓存既然大部分是在高峰期(9~10点)创建的(假设Cache的Expire Time都一样,比如8h),那很有可能失效时间会很接近。几乎同一时间一起失效,这样确实也会引起群起创建的情况,也会导致上面说的击穿的情况发生。
我们在创建同一类型的批次缓存的时候,会采用3-4-3 分布原则。比如一个缓存的Expire Time 是 10H,
那么就是3H + 4H * random() + 3h ,来进行错开!
缺点:同4.1类似,仅仅解决可预见的问题,对突发故障导致的无预期的缓存失效毫无办法。
4.3 消息聚合缓存
为什么每个用户的基本信息都独立存储一个缓存呢?可不可以按照用户类型分片,一类的用户合在一起不是只要查询一次,不会出现峰值期群起攻击数据库的情况。
说明:只有信息修改率非常低的缓存才适合聚合在一个缓存值中,大部分情况下不会这么做。比如你的缓存中聚合了1W个人的信息,Value非常大,但凡其中一个信息修改,那么这个缓存就要更新,不然应用读取到的信息就没有时效性,大Value的缓存频繁的存取是一个很不友好的事情。
用户信息还算修改频率比较低的,你的积分信息,购物车可是很高频变动的,这种的就不能这么干了。
4.4 削峰、加锁、限流
4.4.1 削峰
引进消息队列之类的中间件,将用户的请求放入队列,逐一执行,避免拥挤请求!
4.4.2 加锁
同一个用户的信息查询只让第一个请求进入,进入之后加锁,在获取到数据库信息并更新缓存之后释放锁,
这样单一个信息只请求一次!
4.4.3 限流
为了避免把服务端打挂,在上线前做一次无缓存压测,看数据库与服务端能支撑的最大值。并设置成限流的阈值,保证不会超过服务所能承载的压力,避免过载!
缺点:
- 但凡用锁,排队之类的方案,无一例外的会大幅度降低服务的吞吐率,造成用户长时间等待,体验感下降,这在各大型APP(淘宝、微信、百度APP)上是完全不允许的,也不会这么干。
- 限流也是一样的道理,限流一般是对服务的限流,而难以细粒度到只对某个信息类型的限流。而服务级别限流会误伤其他操作,比如获取排班、排课、获取购物车等非瓶颈的宽松的查询也被限了。当然,现在的限流也可以细粒度到某个或者某几个接口,所以可以将查询用户信息合在一个接口里做一下限流。但是限流也代表部分用户拿不到正确的信息,是一种降级的行为。
备注:数据库也有限流方案,细粒度到这个层级更好
4.5 短暂降级之备选缓存
你的缓存层存在主备场景,他们之间定时异步同步,所以存在短暂数据不一致。
当你的主服务挂了之后,降级去读备服务,数据时效性没那么高,但是也避免了数据库被打穿的情况发生。
4.6 短暂降级值客户端缓存(Redis 6.0)
参考Redis 6.0的 Client Side Cache,看我这篇《追求性能极致:客户端缓存带来的革命》。
类似4.5做法,客户端缓存时效性会差一点,毕竟存在订阅跟同步的过程,数据没那么新。但是避免大量的请求直接上缓存服务,又因无效的缓存服务有把压力转移给数据库。
4.7 短暂降级之空初始值
这是一种短暂降级的方式,大概流程如下:
可以看出,整个过程中我们牺牲了A、B、C、D的请求,他们拿回了一个空值或者默认值,但是这局部的降级却保证整个数据库系统不被拥堵的请求击穿。
5 总结
在不同的场景下各种方法都有各自的优缺点,我们要做的就是根据实际的应用场景来判断和抉择。