本人本科毕业,21届毕业生,一年工作经验,简历专业技能如下,现根据简历,并根据所学知识复习准备面试。
记录日期:2022.1.23
大部分知识点只做大致介绍,具体内容根据推荐博文链接进行详细复习。
文章目录
缓存 - 缓存问题以及解决方案
引入
我们前端发送一个请求,后端接收接请求时先从缓存中取数据,取到直接返回结果;取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。
缓存穿透
描述
正常情况下,我们去缓存中查询数据,有一定几率查询失败,这时就要数据库查询,如果直接去查询一条缓存中和数据库中都不存在的数据,那么这个查询就会百分之百查询到数据库上去。
这种查询称为**“缓存穿透”**,因为去缓存中查询一定会失败,所以请求就会打到数据库上。
带来问题
如果有人恶意拿一个不存在的数据去查询,会产生大量请求,这些请求最终会打到数据库中,数据库可能因为承受不住压力而宕机。
解决方案
接口校验
在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。
缓存空值
碰到查询结果为空的键,放一个空值在缓存中,下次再访问就立刻知道这个键无效,不用查询数据库了。
但是这个方法存在两个问题需要解决:
- 缓存空值需要更多的内存空间,我们可以设置一个比较短的过期时间,过了这个时间,自动剔除这个空值的缓存。此外空值应该与正常值分开存放,否则当空间不足的时候,缓存系统可能有优先剔除正常值,再剔除空值,这个漏洞可能会被攻击。
- 如果某个 key 在缓存中记录为空值,过了一段时间,数据库中添加了这个key,那此时需要利用某种方式来清除这个空值。如果使用的是 Redis 缓存,更新数据后直接在 Redis 中清除即可。
布隆过滤器
使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库。
布隆过滤器详解
本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
布隆过滤器本质就是一个二进制数组。
如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的数组索引置为 1,例如针对值 “baidu”
和三个不同的哈希函数分别生成了哈希值 1、4、7,则会形成下面这种情况:
那我们现在再存一个值 “Ali”
,如果哈希函数返回 2、4、8 的话,上图继续变为:
值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。
现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。
而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。
删除操作
传统的布隆过滤器并不支持删除操作。但是名为 Counting Bloom filter 的变种可以用来测试元素计数个数是否绝对小于某个阈值,它支持元素删除。
参考文章: Counting Bloom Filter 的原理和实现
哈希函数个数以及数组长度的选择
很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。
另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。
所以我们降低误判率的思路就有两种:
- 一个是加大 bitSet 的长度,这样不同的值出现“冲突”的概率就降低了,从而误判率也降低。
- 提升 Hash 函数的个数,Hash 函数越多,每个值对应的 bit 越多,从而误判率也降低。
如何选择适合业务的 k 和 m 值呢?这里直接贴出一个公式:
k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率。
推导过程可以自行查找文章阅读。
缓存击穿
描述
系统中存在以下两个问题时需要引起注意:
- 当前key是一个热点key(例如一个秒杀活动),并发量非常大。
- 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。
在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
解决方案
互斥锁
在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。
关于互斥锁的选择,网上看到的大部分文章都是选择 Redis 分布式锁,因为这个可以保证只有一个请求会走到数据库,这是一种思路。
但是其实仔细想想的话,这边其实没有必要保证只有一个请求走到数据库,只要保证走到数据库的请求能大大降低即可,所以还有另一个思路是 JVM 锁。
JVM 锁保证了在单台服务器上只有一个请求走到数据库,通常来说已经足够保证数据库的压力大大降低,同时在性能上比分布式锁更好。
需要注意的是,无论是使用“分布式锁”,还是“JVM 锁”,加锁时要按 key 维度去加锁。
永不过期
- 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。
- 从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,异步加载数据并更新缓存。
这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还有就是异常情况的处理,如果数据库更新,但是更新缓存失败,一直是脏数据,那可以准备离职报告了。。。
这两种方案对比(优缺)
分布式互斥锁
:这种方案思路比较简单,但是存在一定的隐患,如果在查询数据库 + 和 重建缓存(key失效后进行了大量的计算)时间过长,也可能会存在死锁和线程池阻塞的风险,高并发情景下吞吐量会大大降低!但是这种方法能够较好地降低后端存储负载,并在一致性上做得比较好。永不过期
:这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。
缓存雪崩
描述
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会造成瞬时数据库请求量大、压力骤增,引起雪崩,甚至导致数据库被打挂。
缓存雪崩其实有点像升级版的“缓存击穿”,缓存击穿是一个热点 key,缓存雪崩是一组热点 key。
产生场景
缓存雪崩的缓存失效几种情况:
- 缓存服务器挂了
- 高峰期缓存局部失效
- 热点缓存失效
解决方案
设置key不同超时时间
既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。
不设置key超时时间
不再设置过期时间,也就是说缓存中的数据不会"到期就失效",同时设置一个"逻辑过期时间",过了这个"逻辑过期时间"启动一个线程去数据库中查询最新的内容,然后更新缓存中的值。
这样从根本上杜绝了热点数据失效的问题,但是唯一不足的就是,数据的一致性无法保证。
所以这和缓存击穿一样,需要着重考虑刷新的时间间隔和数据异常如何处理的情况。
加互斥锁
这种方式只允许一个线程来重建缓存,先将其他请求阻塞,等到缓存重建完毕,再重新去缓存中查询。这种方法比较简单,但是可能会存在死锁
和线程池阻塞
的风险。
提高缓存的HA
提高缓存服务的高可用,增加 Redis 集群的节点数量。
服务器雪崩问题(引申)
描述
分布式系统都存在这样一个问题,由于网络的不稳定性,决定了任何一个服务的可用性都不是 100% 的。当网络不稳定的时候,作为服务的提供者,自身可能会被拖死,导致服务调用者阻塞,最终可能引发雪崩连锁效应。
产生场景
应用服务器雪崩产生的几种场景:
流量激增
:比如异常流量、用户重试导致系统负载升高。缓存刷新
:假设A为client端,B为Server端,假设A系统请求都流向B系统,请求超出了B系统的承载能力,就会造成B系统崩溃。程序有Bug
:代码循环调用的逻辑问题,资源未释放引起的内存泄漏等问题。硬件故障
:比如宕机,机房断电,光纤被挖断等…数据库严重瓶颈
:比如:长事务、sql超时等…线程同步等待
:系统间经常采用同步服务调用模式,核心服务和非核心服务共用一个线程池和消息队列。如果一个核心业务线程调用非核心线程,这个非核心线程交由第三方系统完成,当第三方系统本身出现问题,导致核心线程阻塞,一直处于等待状态,而进程间的调用是有超时限制的,最终这条线程将断掉,也可能引发雪崩。
解决方案
一般情况对于服务依赖的保护主要有3种解决方案:
- 熔断方案
- 隔离方案
- 限流方案
熔断模式
熔断模式
主要是参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾。放到我们的系统中,如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
重点监控的机器性能指标有如下:
- cpu(Load) cpu 使用率/负载
- memory 内存
- mysql 监控长事务(这里与 sql 查询超时是紧密结合的,需要重点监控)
- sql 超时
- 线程数
总而言之,除了cpu、内存、线程数外,重点监控数据库端的长事务、sql超时等,绝大多数应用服务器发生的雪崩场景,都是来源于数据库端的性能瓶颈,从而先引起数据库端大量瓶颈,最终拖累应用服务器也发生雪崩,最后就是大面积的雪崩。
解决方案设计
在熔断的设计主要参考了 hystrix
(豪猪哥) 的做法。其中最重要的是三个模块:熔断请求判断算法、熔断恢复机制、熔断报警。
熔断请求判断机制算法
:使用无锁循环队列计数,每个熔断器默认维护10个bucket,每1秒一个bucket,每个blucket记录请求的成功、失败、超时、拒绝的状态,默认错误超过50%且10秒内超过20个请求进行中断拦截。熔断恢复
:对于被熔断的请求,每隔5s允许部分请求通过,若请求都是健康的(RT<250ms)则对请求健康恢复。熔断报警
:对于熔断的请求打日志,异常请求超过某些设定则报警。
隔离模式
隔离模式
模式就像对系统请求按类型划分成一个个小岛的一样,当某个小岛被火少光了,不会影响到其他的小岛。
例如可以对不同类型的请求使用线程池来资源隔离,每种类型的请求互不影响,如果一种类型的请求线程资源耗尽,则对后续的该类型请求直接返回,不再调用后续资源。这种模式使用场景非常多,例如将一个服务拆开,对于重要的服务使用单独服务器来部署,再或者公司最近推广的多中心。
解决方案设计
隔离的方式一般使用两种:
线程池隔离模式
:使用一个线程池来存储当前的请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)信号量隔离模式
:使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃改类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)
限流模式
上述的熔断模式和隔离模式都属于出错后的容错处理机制,而限流模式
则可以称为预防模式
。
解决方案设计
限流模式主要是提前对各个类型的请求设置最高的 QPS 阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应。
服务器超时机制设计
超时分两种,一种是请求的等待超时
,一种是运行超时
。
等待超时
:在任务入队列时设置任务入队列时间,并判断队头的任务入队列时间是否大于超时时间,超过则丢弃任务。运行超时
:直接可使用线程池提供的get方法。
提前发现雪崩
首先让系统不雪崩,然后通过监控发现请求正在接近或者超过阀值,然后再根据具体情况处理,这个接近或者超过阀值的过程,可以称为 “提前发现雪崩”。