dueros模拟测试没有请求后台_实战 | 用手写一个骚气的请求合并,演绎底层的真实...
来源:公众号【 java进阶架构师】
好文推荐:
字节跳动Java岗4面面经分享:索弓|+rabbitmq+spring+Redis
拼多多面经Java开发3面面经:准备好久没想到面试题超级简单
网易严选Java开发三面面经:HashMap+JVM+索引+消息队列
一、服务器崩溃的思考
老板说,他要做个现场营销活动,线上线下都要参与推广,这个活动参与人数可能很大哦··· 果然,由于不是我写的代码,所以那天服务器就崩了,崩的时候很安静,写代码的那个人一个人走的,走的时候很安详。
当请求量到达百万级时候,为啥会崩溃呢?
微服务中是通过接口去向服务提供者发起http请求或者rpc(tcp)请求去获取数据,事实上大量请求中,服务端能处理的请求数量有限,服务中充斥着大量的线程,以及数据库等的连接也会被占用完,导致请求响应速度也越来越慢。
- 响应速度和我们的数据层有关系吗?
- 能不能去添加服务端服务器呢?
- 如果能减少客户端向服务端的请求就好了?
- 限流吗?当前场景能限流吗?
- 每个线程去查询数据,每次都只查询某一个结果,是不是太浪费了?
- 我们能不能想办法,提升我们系统的调用性能?
二、有人想看请求合并,今天她来了
上面的一些思路可以用加缓存,加MQ的方式去解决。但是缓存有限,MQ是个队列,有限流的效果。那么,如何才能提高系统的调用能力,我们学习一下,请求合并,结果分发。
- 正常的请求都是一个请求一个线程,到后台触发相关的业务需求,去调用数据获取业务。
- 当请求合并后,我们要将多个多个请求合并后统一去批量去调用。
大概的设计思路便是如下图所示:
- 常规请求
- 请求合并
- 说下我们的思路
- 解决请求调用多,比如调用商品数据,经过的服务多,调用链很长,所以查询数据库的次数也就非常多,数据库连接池很快就被用光,导致很多请求被阻塞,也导致应用整体线程数非常高。虽然通过增加数据库连接池大小可以缓解问题,并且可以通过压力测试,但这治标不治本。
- 查询商品信息的时候,如果同一商品同一时刻有100个请求,那么其中的99次查询是多余的,可以把100个请求合并成一个真实的后台接口调用,只要控制好线程安全即可。我的想法是使用并发计数器来实现再配合本地缓存,计数器可直接用JDK提供的AtomicInteger,线程安全又提供原子操作。
- 以获取商品信息为例,每个商品id对应一个计数器,计数器初始值默认是0,当一个请求过来后通过incrementAndGet()使计数器自增1并返回自增后的值。当该值等于1,表明该线程在这个时间点上是第一个到达的线程,然后就去调用真实的业务逻辑,在查询到结果后放入到本地缓存中。当该值大于1的时候,表明之前已有线程正在调用业务逻辑,则进入等待状态,并循环的查询本地缓存中是否已有数据可用。获取到结果后都调用decrementAndGet()使计数器减1,计数器被减到0的时候就回到了初始状态,并且当减到0(代表最后一个线程)时清除缓存。
- 那还有在1000次请求中,请求的数据id不同,但是使用的服务接口相同,都是查询商品库的商品id从1~1000的数据,都是从表里面查询,queryDataById(dataId),那我也可以合并这些请求,改为批量查询,然后将数据分发返回。思路就是设计每个请求携带一个请求唯一的traceId,有点像链路跟踪的感觉,简单点可以使用查询的id进行最为跟踪id,将请求放入一个队中,使用定时任务,比如每隔10ms去扫描队列,将这些业务合并请求统一去请求数据库层。
- 此方案有个数据延迟的地方,就是每次循环时的等待状态的时间。因为一次包含多次查库的业务调用,耗时基本都在几十毫秒,甚至是上百毫秒,可以把该等待睡眠设置小一点,比如10毫秒。这样即不会浪费CPU时间,实时性也比较高,但然也可以通过主动唤醒等待线程的方式,这个操作起来就比较复杂些。在这其中还可以添加一些异常处理、超时控制、最大重试次数,最大并发数(超时最大并发数就快速失败)等。
三、开始演练
- 模拟一个远程调用接口
import org.springframework.stereotype.Service;import java.util.*;/** * 模拟远程调用ShopData接口 * @author Lijing */@Servicepublic class QueryServiceRemoteCall { /** * 调用远程的商品信息查询接口 * * @param code 商品编码 * @return 返回商品信息,map格式 */ public HashMap queryShopDataInfoByCode(String code) { try { Thread.sleep(50L); } catch (InterruptedException e) { e.printStackTrace(); } HashMap hashMap = new HashMap<>(); hashMap.put("shopDataId", new Random().nextInt(999999999)); hashMap.put("code", code); hashMap.put("name", "小玩具"); hashMap.put("isOk", "true"); hashMap.put("price","3000"); return hashMap; } /** * 批量查询 - 调用远程的商品信息查询接口 * * @param codes 多个商品编码 * @return 返回多个商品信息 */ public List> queryShopDataInfoByCodeBatch(List codes) { List> result = new ArrayList<>(); for (String code : codes) { HashMap hashMap = new HashMap<>(); hashMap.put("shopDataId", new Random().nextInt(999999999)); hashMap.put("code", code); hashMap.put("name", "棉花糖"); hashMap.put("isOk", "true"); hashMap.put("price","6000"); result.add(hashMap); } return result; }}
- 使用CountDownLatch模拟并发请求的公共测试类
@RunWith(SpringRunner.class)@SpringBootTest(classes = MyBotApplication.class)public class MergerApplicationTests { long timed = 0L; @Before public void start() { System.out.println("开始测试"); timed = System.currentTimeMillis(); } @After public void end() { System.out.println("结束测试,执行时长:" + (System.currentTimeMillis() - timed)); } // 模拟的请求数量 private static final int THREAD_NUM = 1000; // 倒计数器 juc包中常用工具类 private CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM); @Autowired private ShopDataService shopDataService; @Test public void simulateCall() throws IOException { // 创建 并不是马上发起请求 for (int i = 0; i < THREAD_NUM; i++) { final String code = "code-" + (i + 1); // 多线程模拟用户查询请求 Thread thread = new Thread(() -> { try { // 代码在这里等待,等待countDownLatch为0,代表所有线程都start,再运行后续的代码 countDownLatch.await(); // 模拟 http请求,实际上就是多线程调用这个方法 Map result = shopDataService.queryData(code); System.out.println(Thread.currentThread().getName() + " 查询结束,结果是:" + result); } catch (Exception e) { System.out.println(Thread.currentThread().getName() + " 线程执行出现异常:" + e.getMessage()); } }); thread.setName("price-thread-" + code); thread.start(); // 启动后,倒计时器倒计数 减一,代表又有一个线程就绪了 countDownLatch.countDown(); } System.in.read(); }}
- 先来个普通调用演示
/** * 商品数据服务类 * @author lijing */@Servicepublic class ShopDataService { @Autowired QueryServiceRemoteCall queryServiceRemoteCall; // 1000 用户请求,1000个线程 public Map queryData(String shopDataId) throws ExecutionException, InterruptedException { return queryServiceRemoteCall.queryShopDataInfoByCode(shopDataId); }}
- 查询结果展示
开始测试price-thread-code-3 查询结束,结果是:{code=code-3, shopDataId=165800794, price=3000, isOk=true, name=小玩具}price-thread-code-994 查询结束,结果是:{code=code-994, shopDataId=735455508, price=3000, isOk=true, name=小玩具}price-thread-code-36 查询结束,结果是:{code=code-36, shopDataId=781610507, price=3000, isOk=true, name=小玩具}price-thread-code-993 查询结束,结果是:{code=code-993, shopDataId=231087525, price=3000, isOk=true, name=小玩具}....... 省略代码中。。。。price-thread-code-25 查询结束,结果是:{code=code-25, shopDataId=149193873, price=3000, isOk=true, name=小玩具}price-thread-code-2 查询结束,结果是:{code=code-2, shopDataId=324877405, price=3000, isOk=true, name=小玩具}.......共计1000次的查询结果结束测试,执行时长:150
- 那么我们发现我们可以用code作为一个追踪traceId,然后使用ScheduledExecutorService,CompletableFuture,LinkedBlockingQueue等一些多线程技术,就可以实现这个请求合并,请求分发的简单实现demo.
import javax.annotation.PostConstruct;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.concurrent.*;/** * 商品数据服务类 * * @author lijing */@Servicepublic class ShopDataService { class Request { String shopDataId; CompletableFuture> completableFuture; } // 集合,积攒请求,每N毫秒处理 LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); @PostConstruct public void init() { ScheduledExecutorService scheduledExecutorPool = Executors.newScheduledThreadPool(5); scheduledExecutorPool.scheduleAtFixedRate(() -> { // TODO 取出所有queue的请求,生成一次批量查询 int size = queue.size(); if (size == 0) { return; } System.out.println("此次合并了多少请求:" + size); // 1、 取出 ArrayList requests = new ArrayList<>(); ArrayList shopDataIds = new ArrayList<>(); for (int i = 0; i < size; i++) { Request request = queue.poll(); requests.add(request); shopDataIds.add(request.shopDataId); } // 2、 组装一个批量查询 (不会比单次查询慢很多) List> mapList = queryServiceRemoteCall.queryShopDataInfoByCodeBatch(shopDataIds); // 3、 分发响应结果,给每一个request用户请求 (多线程 之间的通信) HashMap> resultMap = new HashMap<>(); // 1000---- 007 for (Map map : mapList) { String code = map.get("code").toString(); resultMap.put(code, map); } // 1000个请求 for (Request req : requests) { Map result = resultMap.get(req.shopDataId); // 怎么通知对应的1000多个线程,取结果呢? req.completableFuture.complete(result); } }, 0, 10, TimeUnit.MILLISECONDS); } @Autowired QueryServiceRemoteCall queryServiceRemoteCall; /** * 1000 用户请求,1000个线程 * * @param shopDataId * @return * @throws ExecutionException * @throws InterruptedException */ public Map queryData(String shopDataId) throws ExecutionException, InterruptedException { Request request = new Request(); request.shopDataId = shopDataId; CompletableFuture> future = new CompletableFuture<>(); request.completableFuture = future; queue.add(request); // 等待其他线程通知拿结果 return future.get(); }}
- 测试结果
开始测试结束测试,执行时长:164此次合并了多少请求:63此次合并了多少请求:227此次合并了多少请求:32此次合并了多少请求:298此次合并了多少请求:68此次合并了多少请求:261此次合并了多少请求:51price-thread-code-747 查询结束,结果是:{code=code-747, shopDataId=113980125, price=6000, isOk=true, name=棉花糖}price-thread-code-821 查询结束,结果是:{code=code-821, shopDataId=568038265, price=6000, isOk=true, name=棉花糖}price-thread-code-745 查询结束,结果是:{code=code-745, shopDataId=998247608, price=6000, isOk=true, name=棉花糖}....... 省略代码中。。。。price-thread-code-809 查询结束,结果是:{code=code-809, shopDataId=479029433, price=6000, isOk=true, name=棉花糖}price-thread-code-806 查询结束,结果是:{code=code-806, shopDataId=929748878, price=6000, isOk=true, name=棉花糖}
可以看到我们将1000次请求进行了合并,数据也是正常的模拟到了。
四、总结
弊端:
- 启用请求的成本是执行实际逻辑之前增加的延迟。
- 如果平均仅需要5毫秒的执行时间,放在一个10毫秒的做一次批处理的合并场景下,则在最坏的情况下,执行时间可能会变为15毫秒。(一定不适合低延迟的RPC场景、一定不适合低并发场景)
场景:
- 如果很少有超过1或2个请求会并发在一起,则没有必要用。
- 一个特定的查询同时被大量使用,并且可以将几+个甚至数百个批处理在一起,那么如果能接受处理时间变长一点点,用来减少网络连接欲,这是值得的。(典型如:数据库、Http接口)
扩展:
- 我们不重复造轮子,在SpringCloud的组件spring-cloud-starter-netflix-hystrix中已经有封装好的轮子Hystrix的HystrixCollapser来实现请求的合并,以减少通信消耗和线程数的占用。
- 当然他的组件比较复杂,也更全面,支持异步,同步,超时,异常等的处理机制。
- 但是,从底层思路来说,无非是线程之间的通信,线程的切换,队列等一些并发编程相关的技术,只要我们高度封装和抽象,那也可以手撸一个合并请求的框架处理。
dueros模拟测试没有请求后台_实战 | 用手写一个骚气的请求合并,演绎底层的真实...相关推荐
- webpack实战之手写一个loader和plugin
webpack实战之编写一个简易的loader和plugin
- jQuery 一次定时器_用 jQuery 手写一个小游戏
作者:王圣松 转发链接:https://juejin.im/post/6844903687307919373 前言 今天给大家带来一个小游戏. 要求:熟悉 JavaScript 继承的概念. 游戏预览 ...
- 未能加载文件或程序集或它的某一个依赖项_手写一个miniwebpack
前言 之前好友希望能介绍一下 webapck 相关的内容,所以最近花费了两个多月的准备,终于完成了 webapck 系列,它包括一下几部分: webapck 系列一:手写一个 JavaScript 打 ...
- 手写一个promise用法_手写一个 Promise
1 js 的基本数据类型? 2 JavaScript 有几种类型的值? 3 什么是堆?什么是栈?它们之间有什么区别和联系? 4 内部属性 [Class] 是什么? 5 介绍 js 有哪些内置对象? 6 ...
- 【北京大学】13 TensorFlow1.x的项目实战之手写英文体识别OCR技术
目录 1 项目介绍 1.1 项目功能 1.2 评估指标 2 数据集介绍 2.1 数据特征 3 数据的预处理 3.1 数据增强 3.2 倾斜矫正 3.3 去横线 3.4 文本区域定位 4 网络结构 5 ...
- python手写一个迭代器_搞清楚 Python 的迭代器、可迭代对象、生成器
很多伙伴对 Python 的迭代器.可迭代对象.生成器这几个概念有点搞不清楚,我来说说我的理解,希望对需要的朋友有所帮助. 1 迭代器协议 迭代器协议是核心,搞懂了这个,上面的几个概念也就很好理解了. ...
- 基于Paddle的计算机视觉入门教程——第7讲 实战:手写数字识别
B站教程地址 https://www.bilibili.com/video/BV18b4y1J7a6/ 任务介绍 手写数字识别是计算机视觉的一个经典项目,因为手写数字的随机性,使用传统的计算机视觉技术 ...
- jquery手写轮播图_用jQuery如何手写一个简单的轮播图?(附代码)
用jQuery如何手写一个简单的轮播图?下面本篇文章通过代码示例来给大家介绍一下.有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助. 用 jQuery 手写轮播图 先上个效果截图: 主要 ...
- dueros模拟测试没有请求后台_小度音箱对接之DuerOS开放平台功能分析
由于项目需要,需要对接DuerOS,使用小度音箱控制设备.近期会对DuerOS技能平台进行一些研究,尤其是智能控制相关功能.特此记录. 文章目录 DuerOS开放平台简介 技能分类 自定义技能 小技能 ...
最新文章
- 同等学力计算机综合难吗,报考2018年同等学力申硕计算机在职研究生毕业很困难吗...
- 微服务容器化最短路径,微服务 on Serverless 最佳实践
- uniapp中实现每次点击左侧菜单右边区域都从顶部开始
- Java基础——线程及并发机制
- HDU_3786 找出直系亲属- softbar
- ASP.NET_ASP.NET Cookies
- 局域网带宽控制解决方案-P2P终结者使用详解
- VS 2015社区版离线下载
- 下载xlsx文件打开一直提示文件已损坏
- 2022年卫浴行业报告:套系化+智能化拓宽边际,箭牌家居内资领航
- asp实训报告摘要_asp制作网页的实训报告总结
- android实现多任务多线程支持断点下载的下载软件
- Windows11 运行安卓子系统 教程
- 《阵列信号处理及MATLAB实现》阵列响应矩阵(均匀线阵、均匀圆阵、L型阵列、平面阵列和任意阵列)
- 攻防世界-MISC:glance-50
- 朝九晚五的程序员如何提高开发技能有感
- 如何调用百度卫星地图
- 圣诞节快到了,教大家用Python画一个简单的圣诞树和烟花,送给那个她
- stm32f103c8t6用什么语言编程,STM32F103ZET6和STM32F103C8T6编程不一样吗
- vsftp客户端_Vsftp使用
热门文章
- Java入参关键字_Java基础17-成员变量、return关键字和多参方法
- android怎么关应用程序,如何关闭Android应用程序?
- python的类写法_Python3 类静态数据的写法
- .NET Core SignalR Redis底板详解(前言)
- HDU3507 Print Article —— 斜率优化DP
- JavaScrip调用腾讯地图
- 使用gnucash查看任意时间段内的所有者权益变动表
- ASP.NET Core 源码阅读笔记(5) ---Microsoft.AspNetCore.Routing路由
- 同一公司代码下工厂间的库存转储 (轉載)
- idea java 注释模板配置