关于 SpringBoot 中的并发请求外部接口的需求解惑

338 天前
 Koril

前言

老哥们,现在碰到一个需求,希望大家帮忙看看,有什么方案。 我是 Java 菜鸡,可能提到的某些点很傻很无知,望见谅。


需求

后台有这样一个接口 /demo ,前端请求到 /demo 后,代码需要按照顺序访问多个外部 HTTP 接口

比如外部接口有三个:

  1. /api-a
  2. /api-b
  3. /api-c

前端请求 /demo 后,后端直接返回 response 200 "ok" 就行,不用阻塞。

每个外部接口的一些传参都依赖于前一个请求的返回值(/api-c 的传参依赖于/api-b ),所以顺序是一定的,只能一个个请求。

最终结果(最后一个接口 /api-c 响应后),返回结果存入数据库。

另外,中间可能会出现 timeout 或者其他异常,这些信息也需要存入数据库。

问题在于,请求的并发量略微大了些,大概一秒钟有上百个请求进来(可以简化成每一秒就有 120 个请求进入该接口)。


机器环境、语言、框架、数据库

机器:单个虚拟机,CPU 和内存都可以按需求往上调大,目前是 18 核 48GB 的配置。

语言:Java 21 (抱歉,其他语言不会,只能用这个)

框架:SpringBoot3

数据库:PG 、Redis 、Mongo (随意使用)


我自己的方案

我的方法很直接,把请求的外部方法的代码放在一个 service 函数里,然后加 @Async 注解。

然后配置 ThreadPoolTaskExecutor (就是网上都能搜到的那些配置)。

另外,为了追踪每一个任务线程的结果,在线程里,一开始就生成一个 UUID ,然后构造一个对象,每一步都把相应的信息(成功或者失败)存入这个对象,最后以这个 UUID 为主键存储到数据库里。


有更好的解决方案么

按照我自己的观察,如果线程数量给小了,就容易产生队列堆积,给大了,又不确定该给多大,难道只能测试?

我的理解大概是 100 个请求进来,假设外部 3 个接口,每个需要 5 秒,那么全部请求完就是 15 秒(忽略其他时延),100 * 15 = 1500 个线程,如果小于这个值,就会堆积在队列中。

我想知道是否能根据以下的变量,通过某种方法推算出这个接口的理论的上限?

  1. 机器配置( CPU 个数,内存大小,上下行带宽等)
  2. 请求外部接口的个数,平均每个外部接口的响应时间
  3. 其他参数

怎么计算,并且达到这个上限?有什么更好的方法么?

4959 次点击
所在节点    程序员
56 条回复
yc8332
337 天前
搞个队列啊,前端请求过来就记下来然后直接返回,自己开个线程或者进程慢慢去跑所有记下来的请求
ymy3232
337 天前
加线程池监控慢慢调就行
xuanbg
337 天前
你这个不是后面的依赖前面的数据吗?直接一个方法里面挨个请求呀,用得着 @Async 么? api/a 的数据没回来,你调 api/b 时怎么传参?接口超时不应该抛出异常么?通过捕获异常来打日志就行。

至于你每秒钟有 120 请求,这就看你的/demo 接口的并发能力了。压测一下就知道 QPS 了,然后根据需要的机器数量,在 nginx 上做个负载均衡就行。
xianqin
337 天前
歪个楼,对于会前端的 Java 菜鸡。
先不考虑性能和数据完整性问题,java 有类似前端这样的写法吗?
demoB = async ()=>{
const a = await api_a()
const b = await api_a(a)
const c = await api_a(b)
await save(c)
}

demo = ()=>{
demoB()
return 200
},
Leviathann
337 天前
@xianqin 全 await 不就是 java 默认的同步调用?
coderYang
337 天前
我觉得你的描述是,1 、前端请求后,后端只要接收到请求,即可返回结果,业务操作可异步进行
2 、异步进行时,无法确认自己线程池该给多少线程是最佳方案
3 、链式调用的时延较高,异步等待时间过长,队列堆积

个人想法:当无法确认外部接口的响应时间时,可通过 MQ 进行消息传递。
三个 TOPIC ,和你的思路一致,每当有一个/demo 被请求,则直接发送消息与参数至 TOPIC-A 中,然后 CONSUMER-A 去处理。A 处理完则发消息到 B ,B 消费完则发送消息至 C 并被消费。
通过 MQ 的方式,首先可以保证消息不丢失,且链式不出问题,日志记录、报错回滚与重试都更方便。
至于 MQ 消费者的线程个数,这个没所谓的,基本上都是有则新建线程,等待一段时间后回收线程。
其次如果觉得一个线程同一时间只能消费一条消息太慢的话,可以批量消费,通过 Future.get 来实现异步。
jdk21 的虚拟线程不太懂。
0NF09LJPS51k57uH
337 天前
spring5 的 webclient ,基于 reactor 模型的,a 接口的 subscribe 中调 b 接口并结果落库,b 的 subscribe 中调 c 接口并结果落库,以此类推。
workqing2023
337 天前
这个其实主要看你是每秒都有这么多请求,还是只是偶尔有这么多请求,如果每秒都是的话,你的消费者就得大,无论是线程还是消息队列都一样,不然就会一直堆积;如果只是偶尔这么多请求,线程池小一点也没关系,慢慢消化就行了
skallz
337 天前
我也遇到过类似的需求,只不过那个需求更加消耗资源,直接丢到 serverless 了,想调几百几千次都行,哈哈,把并发的烦恼完全抛掉
owen800q
337 天前
@Leviathann demo 接口調用處是異步的,不是阻塞
ychost
337 天前
虚拟线程或者用 Webflux 非阻塞来实现超高并发,当然要求底层数据库访问也要支持
Nosub
337 天前
webflux 或是 RXjava ,前端就是 rxjs ,就是专门用来解决这类问题的,建议你了解下响应式编程,或是说函数式编程,不过如果你是习惯了面向对象编程可能不是太习惯,楼上很多回答都极不专业。
mgcnrx11
337 天前
@Nosub 赞同。特别是最后一句,我看到这种问题都要上 mq 都血压上升
siweipancc
336 天前
哥,信号量啊
onedayrexgmail
336 天前
首先,你要知道你这个需求的瓶颈在哪,从你的描述中看出来,目前瓶颈在于调用的第三方接口,这些三方接口由于某些原因,可能会导致你的系统堆积,从而导致你的系统受影响,响应慢甚至导致崩溃,这个需求里面,首先第一点你要解决的问题是不要让第三方接口影响到你的系统,前端来调用你使用异步方式去调用三方接口这是一个解决方案,第二点你要解决的就是如何更高效的去调用第三方的接口。这里我说下我的方案,首先,前端调用进来我会去存一张主任务表,一个请求就是一个任务,同时有一张任务明细表,明细表就写的是你调用的 a 、b 、c 接口了,第一次初始化肯定是 a 这个明细能调用,当调用完了 a 再去更新 b 让 b 这条子任务也可调用,这样你的前端调用你时就相当于你只初始化任务数据,其它不管。第二步,写一个定时或者在你前端调用你初始化完成后,做一个通知,把你的任务加到你的执行队列里面去,这里我们先说定时任务,定时任务查询子任务表,哪些任务可以执行,查多少条,这个需要看你的第三方的接口限流情况与你自身带宽情况来定,这个可以做一个配置,发布后根据情况调整,比如每次同时请求的数量先写 50 ,如果三方限制或者带宽限制,调低一些,这个参数需要慢慢调,这里可以写得更详细一些,做三个配置,每一种类型的接口写一个,同时开三个线程池来请求这三类接口,比如 a 接口线程池开 10 个,b 接口线程池开 20 个,具体需要调试,然后请求到结果后把结果写回任务子表中 a ,标识 a 完成,同时把结果写到 b 作为 b 的入参,b 同时标识为可调用,下一次定时任务就会来执行这条 b ,b 里面逻辑同 a ,一直到 c ,c 是一个终止任务,他完成了把 c 自己标识成完成,同时还要标识下主任务完成,但是这里面要注意防止上一次定时任务还没执行完就又到下一论时间再执行,可以写一个锁去做判断,同一时间同种类的定时任务只能有一个,比如执行 a 接口的定时任务,在同一时间只有有一个在执行,其它到时间了来执行都先不执行,这种方式来做,一是你可以对每个接口去动态做调整,二是你可以对中间执行失败的任务再次进行尝试,甚至极端一些有可能需要你手动修改参数的你也可以去调整,三是你们前端还能根据你的状态来查询这个任务具体执行的一个状态。这样既解决了三方接口导致你响应慢的问题,又解决了你能高效的去调用三方的这些接口的问题。
iiinspiration
335 天前
mq+协程呗 前端的请求扔到 mq 然后协程那一直跑着就完事了

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

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

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

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

© 2021 V2EX