密集多次 http 请求外部接口怎么操作比较好?

2022-04-21 10:02:18 +08:00
 Dogod37
先行感谢各位大佬解答。背景如下:
#1 本人 Java 菜鸡一枚。所以有些问题可能有些白痴。
#2 一个 Spring Boot 的 Web 应用,一台阿里金融云 ECS ( 2 core 8GB ),后接一台阿里金融云 RDS ,无 redis 等中间件。
#3 目标场景是用户从页面提交业务数据后,后端要将业务数据写入 MySQL ,并组装 Xml 报文后 http 发往第三方接口,从第三方接口获取返回数据后,再返回给 Web 页面,也就是说这个过程中,Web 页面是一直在等待后端同步返回结果的(有进度条样式一直在转)。而这个场景有 10%的部分是要拆分成最多 30 份,对应地去请求外部接口 30 次的( 30 份数据来自一次页面提交)。
#4 外部接口性能较差,一次请求耗时平均为 5s 左右,请求高峰期可能会达到 20s 。

因为业务的特性,会倾向于用户在页面点击提交按钮后很快( 30s 内)得到后端返回的处理结果。所以如果 30 份数据用串行的方式去请求外部接口,那最理想情况也是 5 * 30s=150s 了。所以问题是:
#1 能不能通过 CompletableFuture ,注入自定义的线程池(而非 JVM 的线程池),同时开启 30 个线程去并行执行这些外部请求。简单测试过外部接口对于并发请求的表现,100 个并发请求,1/10 响应用时 5s ,1/10 响应用时 10 几 s...最后 1/10 响应用时需要 40s (可接受)。
#2 上述方案一个 Web 提交就可能要开启 30 个线程,虽然这种需要开启 30 个线程的页面提交基本上不会一下子进来两个。但是!如果真的就有两个或者三个客户在同时触发了这个场景,需要考虑些什么吗?避免带来不可预料的异常或者崩溃。
#3 上述方案如果不可行的话,有没有更合理的解决方案?期望是用户页面同步得到结果,不要异步的....会增加复杂性,搞不动了...

多谢各位,帮忙孩子...
3751 次点击
所在节点    Java
24 条回复
cheng6563
2022-04-21 10:19:39 +08:00
创建线程池可以限制线程数量:new ThreadPoolExecutor(0, 60, 3, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy());
新建线程运行任务,用一个 CountDownLatch 进行任务计数,他可以阻塞直到 30 个任务完成。

没事别用 Future ,烦人的很
hay313955795
2022-04-21 10:20:21 +08:00
不想异步拿数据,那么 java8 的并行流应该可以满足吧
Dogod37
2022-04-21 10:28:01 +08:00
@cheng6563 没说清楚,Controller 层接到请求后,去调用方法并行执行这些请求,阻塞到任务全部完成后,想要这些任务又返回值,Controller 拿着这些返回值处理并返回给页面,您说的这种方式应该没有返回值?
Dogod37
2022-04-21 10:44:44 +08:00
@hay313955795 30 个 I/O 耗时操作的,并行流应该不太行....
liuhan907
2022-04-21 10:53:49 +08:00
既然你不想要异步,那并行流和线程池没有区别呀
dqzcwxb
2022-04-21 10:55:32 +08:00
并行?还要处理结果?那必然是 Completablefuture 啦
agzou
2022-04-21 11:00:23 +08:00
线程池+Future+CountDownLatch
Dogod37
2022-04-21 11:04:46 +08:00
@dqzcwxb 一次开 30 个线程属不属于不合理操作了?
gesse
2022-04-21 11:14:17 +08:00
设计得不好, 用户一刷新就全部 GG 了
v2orz
2022-04-21 11:14:32 +08:00
既然你有“拆分成最多 30 份”的需求,那这么做看起来也没啥问题
slomo
2022-04-21 11:21:36 +08:00
@Dogod37 如果每次请求这个接口, 你都开 30 个, 算不合理操作;
如果你把 30 个线程的线程池作为一个 bean 注入, 每次调用这个接口, 都用这个线程池来跑, 就不算.
一般网络 IO 的阻塞系数大概是 0.8~0.9, 也就是说线程处理一个网络请求, 其中等待 remote 返回的时间大概占 80%到 90%, 这时候推荐创建的线程池线程数量是 CPU 核数 /(1 - 阻塞系数).
当然这只是理论上的, 还是得多次操作看具体.
可以用 CompletableFuture 做, 最后用 CompletableFuture.allOf 来阻塞等待完成
wolfie
2022-04-21 11:26:43 +08:00
平均 5s ,高峰 20s ,最多 30 次请求,期望 30s 内,不用一次性使用 30 个线程。

防止多用户并行请求的话,固定几个令牌,同时超了直接拒绝请求。

用户请求多,第三方数据量不大,能接收一定延时性的话,可以考虑定时拉取。
adoal
2022-04-21 11:37:13 +08:00
重新设计交互逻辑,用户提交后之后扔到独立的沃克调度器里去做,页面上直接返回,告诉用户去执行了,要到它自己的任务面板里刷新看结果,调度器里看到任务执行完后更新 web 这边的任务状态表。
cheng6563
2022-04-21 11:40:31 +08:00
@Dogod37 你拿个 CopyOnWriteArrayList 之类的东西存着线程的返回值就行了,CountDownLatch 可以保证你全部线程执行完毕后再继续运行。
Joker123456789
2022-04-21 11:41:35 +08:00
为什么你觉得异步 会增加复杂性? 这个场景就是适合异步啊, 你用同步 就必然需要多线程,而且线程如果太多 不见得会增加性能。 并且线程也不会全部同步执行啊,要看 CPU 核心数的, 还有上下文切换的负担。 最重要的是,你再怎么优化 也优化不到 5 秒以下的。

最简单的方法就是,提交归提交,响应归响应。 提交后,在表里插一条提交记录,然后直接给页面一个响应,后端异步处理, 单独做一个页面,用来展示 这些提交记录。 后端异步处理完成后,修改对应的记录状态就好了。

如果处理失败了,也可以把异常信息 写入表里(每条 提交记录,都带一个异常信息字段)。 还可以在页面上做一个重试按钮。
Joker123456789
2022-04-21 11:44:15 +08:00
还有,gesse 说的,等待响应期间 如果用户刷新一下就 GG 了
Tom7
2022-04-21 11:50:11 +08:00
不清楚具体业务,给一个体验思路,后端全异步,前端提交后可以根据提交 id 之类的循环查询结果,如果异常,通过状态跳过,避免了长时间等待,又可以直观的看到每个任务结果
golangLover
2022-04-21 12:35:25 +08:00
可以,我自己就是经常用 completablefuture 做的
byte10
2022-04-21 12:41:03 +08:00
你可以看看我的答案,完美符合你的需求。https://www.bilibili.com/video/BV1FS4y1o7QB , 用 tomcat 的 NIO ,异步 servlet api ,完全没压力,你个机器单机 1000/s 吞吐量都可以。客户端找一个响应式的,或 vert.x 生态的也有,我代码给出了例子。不要先判断对错,要先看。看完,就会明白。当然 CompletableFuture 。allOf 也得用上,不然不好控制多个任务的同步问题
byte10
2022-04-21 13:07:25 +08:00
可执行代码:

```
@PostMapping("/test")
public void postUrl(HttpServletRequest req) {
final AsyncContext ctx = req.startAsync();
List<String> respList = new ArrayList<>();
int taskNum = 10;
CountDownLatch countDownLatch = new CountDownLatch(taskNum);
for (int i = 0; i < taskNum; i++) {
VertxHttpClientUtil httpClientUtil = new VertxHttpClientUtil();
httpClientUtil.post("https://baidu.com", (resp) -> {
respList.add(resp);
countDownLatch.countDown();
});
}
CompletableFuture.supplyAsync(() -> {
try {
countDownLatch.await();
System.out.println("finish..");
ctx.getResponse().getWriter().print(respList.get(0));
ctx.complete();
} catch (Exception e) {
e.printStackTrace();
}
return null;
});
}

```
CompletableFuture ,请自行设置 50-200 个线程,瓶颈在于 CompletableFuture 的线程数和你第三接口的吞吐量了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/848291

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX