这是why哥的第 76 篇原创文章前端
前段时间一个在深圳的,两年经验的小伙伴出去面试了一圈,收割了几个大厂 offer 的同时,还总结了一下面试的过程当中遇到的面试题,面试题有不少,文末的时候我会分享给你们。java
此次的文章主要分享他面试过程当中遇到的一个场景题:web
他说对于这个场景题,面试的时候没有什么思路。面试
说真的,请求合并我知道,高并发无非就是快速的请求合并。redis
可是在我有限的认知里面,若是相似于秒杀的高并发扣库存这个场景,用请求合并的方式来作,我我的感受是有点怪怪的不够传统。算法
在传统的,或者说是业界经常使用的秒杀解决方案中,从前端到后台,你也找不到请求合并的字样。sql
我理解请求合并更加适用的场景是查询类的,或者说是数值增长类的需求,对于库存扣减这种,你稍不留神,就会出现超卖的状况。数据库
固然也有多是我理解错题意了,看到高并发扣库存就想到秒杀场景了。后端
可是不重要,咱们也不能直接和面试官硬刚。设计模式
我会从新给个我以为合理的场景,告诉你们我理解的请求合并和高并发下的请求合并是什么玩意。
如今咱们抛开秒杀这个场景。
换一个更加合适,你们可能更容易理解的场景来聊聊什么是请求合并。
就是热点帐户。
什么是热点帐户呢?
在第三方支付系统或者银行这类交易机构中,每产生一笔转入或者转出的交易,就须要对交易涉及的帐户进行记帐操做。
记帐通常来讲涉及到两个部分。
若是对于某个帐户操做很是的频繁,那么当咱们对帐户余额进行操做的时候,就会涉及到并发处理的问题。
并发了怎么办?
是的,咱们能够对帐户进行加锁处理。这样一来,这个帐户就涉及到频繁的加锁解锁操做。
这样咱们能够保证数据不出问题,可是随之带来的问题是随着并发的提升,帐户系统性能降低。
这个帐户,就是热点帐户,就是性能瓶颈点。
热点帐户是业界的一个很是常见的问题。
我所了解到的常规解决方案大概能够分为三种:
本小节主要是介绍“多笔合一记帐”解决方案,从而引出请求合并的几率。
对于另外两个解决方案,就先简单的说一下。
首先异步缓冲记帐。
我先不解释,你就看着这个名字,想着这个场景,你以为你会想到什么?
异步,是否是想到了 MQ?
那么请问你系统里面为何要引入 MQ 呢?
来,面试八股文背起来:异步处理、系统解耦、削峰填谷。
你说咱们当前的这个场景下属于哪种状况?
确定是为了作削峰填谷呀。
假设帐务系统的 TPS 是 200 笔每秒,当请求低于 200 笔每秒的时候,帐务服务基本上可以及时处理立刻返回。
从用户的角度来讲就是:啪的一下,很快啊。我就收到了记帐成功的通知了,也看到帐户余额发生了变化。
可是在业务高峰期的时候,流量直接翻倍,每秒过来了 400 笔请求,这个时候对于帐务系统来讲就是流量洪峰,须要进行削峰了,队列里面开始堆积着请求,开始排队处理了。
在流量低谷的时候,就能够把这部分数据消费完成。
至关于数据扔到队列里面以后,就能够告诉用户记帐成功了,钱立刻就到。
可是这个方案带来的问题也是很明显的,若是流量真的爆了,一天都没有谷让你填,队列里面堆积着大量的请求还没来得及处理,你怎么办?
这对于用户而言就是:你明明告诉我记帐成功了,为何个人帐户余额迟迟没有变化呢?是否是想阴我钱,我反手就是一波投诉。
另一个风险点就是对于支出类的请求,若是被削峰,很明显,咱们提早就告诉了用户操做成功,可是真正动帐户余额的时候已经延迟了,因此可能会出现帐户透支的状况。
另一个设立影子帐户的方案,其实和咱们本次的请求合并的主题是另一个不一样的方向。
它的思想是拆分。
热点帐户说到底仍是一个单点问题,那么对于单点问题,咱们用微服务的思想去解决的话是什么方案?
就是拆分。
假设这个热点帐户上有 100w,我设立 10 个影子帐户,每一个帐户 10w ,那么是否是咱们的流量就分散了?从一个帐户变成了 10 个帐户。
压力也就进行了分摊。
这个方案就有点相似于秒杀场景中的库存了,库存咱们也能够拆多份。
可是带来的问题也很明显。
一是获取帐户余额的时候须要进行汇总操做。
二是假设用户要扣 11w 呢?咱们总余额是够的,可是每一个影子帐户上的钱是不够的。
三是你的影子帐户选择的算法是很重要的,是用随机?轮训?加权?这些对于帐务成功率都是有比较大的影响的。
另外这个思想,我在以前的文章中也提到过,有兴趣的能够看看其在 JDK 源码中的应用:我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字...
好了,回到本次的主题:多笔合一笔记帐。
有个网红店,生意很是的好,天天不少人在店里面消费。
当用户扫码支付后,请求会发送到这个店对接的第三方支付公司。
当支付公司收到请求,并完成记帐操做后才会告知商户用户支付成功。能够给用户商品了。
随着店里生意愈来愈好,带来的问题是第三方支付公司的系统压力增长,扛不住这么大的并发了。致使用户支付成功率的降低或者用户支付成功后很长时间才通知到商户。
那么针对这个商户的帐户,咱们就能够作多笔合一笔处理。
当记录进入缓冲流水记录表以后,咱们就能够通知商户用户支付成功了,至于钱,你放心,我有定时任务,一会就到帐:
因此当用户下单以后,咱们只是先记录数据,并不去实际动帐户。等着定时任务去触发记帐,进行多笔合并一笔的操做。
好比下面的这个示意图:
商户实际有 5 个用户支付记录,可是这 5 笔记录对应着一条帐户流水。咱们拿着帐户流水,也是能够追溯到这 5 笔交易记录的。
这样的好处是吞吐量上来了,通知及时,用户体验也好了。可是带来的弊端是余额并非一个准确的值。
假设咱们的定时任务是一小时汇总一次,那么商户在后端看到的交易金额多是一小时以前的数据。
并且这种方案对于帐户收钱的场景很是的适合,可是减钱的场景,也是有可能会出现金额为负的状况。
不知道你有没有看出多笔合一笔处理方案的秘密。
若是咱们把缓冲流水记录表看做是一个队列。那么这个方案抽象出来就是队列加上定时任务。
因此,_请求合并的关键点也是队列加上定时任务_。
文章看到如今,请求合并咱们应该是大概的了解到了,也确实是有真实的应用场景。
除了我上面的例子外,好比还有 redis里面的 mget,数据库里面的批量插入,这玩意不就是一个请求合并的真实场景吗?
好比 redis 把多个 get 合并起来,而后调用 mget。屡次请求合并成一次请求,节约的是网络传输时间。
还有真实的案例是转帐的场景,有的转帐渠道是按次收费的,那么做为第三方公司,咱们就能够把用户的请求先放到表里记录着,等一小时以后,一块儿汇总发起,假设这一小时内发生了 10 次转帐,那么 10 次收费就变成了 1 次收费,虽然让客户等的稍微久了点,但仍是在能够接受的范围内,这操做节约的就是真金白银了。
理解了请求合并,那咱们再来讲说当他前面加上高并发这三个字以后,会发生什么变化。
首先不管是在请求合并的前面加上多么狂拽炫酷吊炸天的形容词,说的多么的天花乱坠,它也仍是一个请求合并。
那么队列和定时任务的这个基础结构确定是不会变的。
高并发的状况下,就是请求量很是的大嘛,那咱们把定时任务的频率调高一点不就好了?
之前 100ms 内就会过来 50 笔请求,我每收到一笔就是当即处理了。
如今咱们把请求先放到队列里面缓存着,而后每 100ms 就执行一次定时任务。
100ms 到了以后,就会有定时任务把这 100ms 内的全部请求取走,统一处理。
同时,咱们还能够控制队列的长度,好比只要 50ms 队列的长度就达到了 50,这个时候我也进行合并处理。不须要等待到 100ms 以后。
其实写到这里,高并发的请求合并的答案已经出来了。关键点就三个:
一是须要借助队列加定时任务实现。
二是控制定时任务的执行时间.
三是控制缓冲队列的任务长度。
方案都想到了,把代码写出来岂不是很容易的事情。并且对于这种面试的场景图,通常都是讨论技术方案,而不太会去讨论具体的代码。
当讨论到具体的代码的时候,要么是对你的方案存疑,想具体的探讨一下落地的可行性。要么就是你答对了,他要准备从代码的交易开始衍生另外的面试题了。
总之,大部分状况下,不会在你给了一个面试官以为错误的方案以后,他还和你讨论代码细节。大家都不在一个频道了,赶忙换题吧,还聊啥啊。
实在要往代码实现上聊,那么大几率他是在等着你说出一个框架:Hystrix。
其实这题,你要是知道 Hystrix,很容易就能给出一个比较完美的回答。
由于 Hystrix 就有请求合并的功能。给你们演示一下。
假设咱们有一个学生信息查询接口,调用频率很是的高。对于这个接口咱们须要作请求合并处理。
作请求合并,咱们至少对应着两个接口,一个是接收单个请求的接口,一个处理把单个请求汇总以后的请求接口。
因此咱们须要先提供两个 service:
其中根据指定 id 查询的接口,对应的 Controller 是这样的:
服务启动起来后,咱们用线程池结合 CountDownLatch 模拟 20 个并发请求:
从控制台能够看到,瞬间接受到了 20 个请求,执行了 20 次查询 sql:
很明显,这个时候咱们就能够作请求合并。每收到 10 次请求,合并为一次处理,结合 Hystrix 代码就是这样的,为了代码的简洁性,我采用的是注解方式:
在上面的图片中,有两个方法,一个是 getUserId,直接返回的是null,由于这个方法体不重要,根本就不会执行。
在 @HystrixCollapser 里面能够看到有一个 batchMethod 的属性,其值是 getUserBatchById。
也就是说这个方法对应的批量处理方法就是 getUserBatchById。当咱们请求 getUserById 方法的时候,Hystrix 会经过必定的逻辑,帮咱们转发到 getUserBatchById 上。
因此咱们调用的仍是 getUserById 方法:
一样,咱们用线程池结合 CountDownLatch 模拟 20 个并发请求,只是变换了请求地址:
调用以后,神奇的事情就出现了,咱们看看日志:
一样是接受到了 20 个请求,可是每 10 个一批,只执行了两个sql语句。
从 20 个 sql 到 2 个 sql,这就是请求合并的威力。请求合并的处理速度甚至比单个处理还快,这也是性能的提高。
那假设咱们只有 5 个请求过来,不知足 10 个这个条件呢?
别忘了,咱们还有定时任务呢。
在 Hystrix 中,定时任务默认是每 10ms 执行一次:
同时咱们能够看到,若是不设置 maxRequestsInBatch,那么默认是 Integer.MAX_VALUE。
也就是说,在 Hystrix 中作请求合并,它更加侧重的是时间方面。
功能演示,其实就这么简单,代码量也很少,有兴趣的朋友能够直接搭个 Demo 跑跑看。看看 Hystrix 的源码。
我这里只是给你们指几个关键点吧。
第一个确定是咱们须要找到方法入口。
你想,咱们的 getUserById 方法的方法体里面直接是 return null,也就是说这个方法体是什么根本就不重要,由于不会去执行方法体中的代码。它只须要拦截到方法入参,并缓存起来,而后转发到批量方法中去便可。
而后方法体上面有一个 @HystrixCollapser 注解。
那么其对应的实现方式你能想到什么?
确定是 AOP 了嘛。
因此,咱们拿着这个注解的全路径,进行搜索,啪的一下,很快啊,就能找到方法的入口:
com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect#methodsAnnotatedWithHystrixCommand
在入口处打上断点,就能够开始调试了:
第二个咱们看看定时任务是在哪儿进行注册的。
这个就很好找了。咱们已经知道默认参数是 10ms 了,只须要顺着链路看一下,哪里的代码调用了其对应的 get 方法便可:
同时,咱们能够看到,其定时功能是基于java.util.concurrent.ScheduledThreadPoolExecutor#scheduleAtFixedRate
实现的。
第三个咱们看看是怎么控制超过指定数量后,就不等待定时任务执行,而是直接发起汇总操做的:
能够看到,在com.netflix.hystrix.collapser.RequestBatch#offer
方法中,当 argumentMap 的 size 大于咱们指定的 maxBatchSize 的时候返回了 null。
若是,返回为 null ,那么说明已经不能接受请求了,须要当即处理,代码里面的注释也说的很清楚了:
以上就是三个关键的地方,Hystrix 的源码读起来,须要下点功夫,你们本身研究的时候须要作好心理准备。
最后再贴一个官方的请求合并工做流程图:
打完收工。
前面说的深圳的,两年经验的小伙伴把面试题汇总了一份给我,我也分享给你们吧。
Java基础
JVM相关
Redis相关
SQL相关
Spring相关
Dubbo相关
分布式相关
设计模式
Zookeeper
MQ
计算机网络
Tomcat
代码
场景问题
说来惭愧,有些题我也答不上来,因此和你们一块儿查漏补缺吧。
哦,对了,那个小伙子最终收割了好几个大厂 offer,跑来问我哪一个 offer 好。
你说这问题对我来讲那不是超纲了吗?我也没在大厂体验过啊。因此我怀疑他不讲武德,来骗,来偷袭我这个老实巴交的小号主,我但愿他能耗子尾汁,在鹅厂好好发展:
才疏学浅,不免会有纰漏,若是你发现了错误的地方,能够提出来,我对其加以修改。
感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。
我是 why,一个被代码耽误的文学创做者,不是大佬,可是喜欢分享,是一个又暖又有料的四川好男人。
还有,欢迎关注我呀。