V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Koril
V2EX  ›  程序员

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

  •  
  •   Koril · 14 天前 · 3752 次点击

    前言

    老哥们,现在碰到一个需求,希望大家帮忙看看,有什么方案。 我是 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. 其他参数

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

    56 条回复    2024-11-21 11:12:46 +08:00
    Goooooos
        1
    Goooooos  
       14 天前   ❤️ 1
    消息队列
    qinxi
        2
    qinxi  
       14 天前
    换个思路, 用 MQ 去处理, 单独的服务去消费, 开几台消费者丰俭由人. 反正前端不用等结果. 不会影响接收请求的服务
    cheng6563
        3
    cheng6563  
       14 天前
    都 Java21 了,直接整虚拟线程了,都不用管线程数。
    lucasdev
        4
    lucasdev  
       14 天前
    "Java 21"? 要素察觉,虚拟线程
    249239432
        5
    249239432  
       14 天前
    数据要尽可能不丢失就消息队列
    用 java 自带的队列也可以,简单方便
    vZexc0m
        6
    vZexc0m  
       14 天前
    消息队列
    dode
        7
    dode  
       14 天前
    使用多个长连接,循环请求这个接口,检查上传带宽占用多少
    kd9yYw2RyhQwAwzn
        8
    kd9yYw2RyhQwAwzn  
       14 天前
    消息队列/线程池都行
    lucasdev
        9
    lucasdev  
       14 天前
    @lucasdev VirtualThreadTaskExecutor
    spritecn
        10
    spritecn  
       14 天前
    随便怎么写,先加个监控布上去,有大量需求再调
    TUNGH
        11
    TUNGH  
       14 天前
    用消息队列也可以,用线程池也行,如果是线程池,你要确定并发数有多少,确定并发时间会持续的时长,三个外部接口,每个请求需要花多少时间,外部接口能不能接受你的高并发要求?你要根据以上几点来考虑如何设置线程池大小.
    xubeiyou
        12
    xubeiyou  
       14 天前
    mq 合适 如果你希望维护这个 MQ 就是用 eventbus 谷歌的 你可以理解为一种不需要部署的内部 MQ 或者就是线程池起线程监听 处理
    chen11
        13
    chen11  
       14 天前
    用队列缓存前端进来的请求,后面开多少线程不是就随便搞
    byte10
        14
    byte10  
       14 天前
    NIO 可以的,比较清晰看到。当然虚拟线程也可以 ,但是要用对才行。
    yangyuhan12138
        15
    yangyuhan12138  
       14 天前
    可以试试响应式编程,因为你的后一个 api 请求依赖前一个的结果,这个的吞吐量理论上也很高,关键词 Reactive
    StoneHuLu
        16
    StoneHuLu  
       14 天前
    直接起个 mq ,接口就直接推消息给 mq 就行了,然后写个消费程序,接受 mq 消息然后处理业务逻辑,如果程序报错就不要 ack ,走重试逻辑,如果请求数量很多并且是不间断地,处理不过来的话,就加消费程序就行了,这是标准处理方式
    liaohongxing
        17
    liaohongxing  
       14 天前
    用虚拟线程最合适 ,虚拟线程依次串行访问 。还能大并发
    zzz2Z
        18
    zzz2Z  
       14 天前   ❤️ 2
    mq 或者把任务存到数据库, 在通过定时任务/线程池拉取任务执行
    flmn
        19
    flmn  
       14 天前
    这种问题的标准解法是消息队列,如果没有 kafka/rabbit ,那么 redis 的 list 可以一用。
    yikuo
        20
    yikuo  
       14 天前
    用 reactor 的异步 http 请求或者用虚拟线程
    Koril
        21
    Koril  
    OP
       14 天前
    使用 MQ 会不会堆积呢,需求是前端对 /demo 发起请求后,后端逻辑(就是顺序请求外部接口的方法)必须要立即执行,引入中间件会不会增加中间的时延。
    yangyaofei
        22
    yangyaofei  
       14 天前
    如果不想把程序写很大(MQ + 消费者之类的), 直接请求进来存数据库, 对应某个字段为任务是否执行, 然后 scheduler 去找没有执行的数据, 执行并把结果添上, scheduler 后面想加多少线程就随意了.

    其实就是相当于用数据库直接当 mq 用, 前台请求只是插入挺简单的速度回很快
    wu00
        23
    wu00  
       14 天前
    这还不上队列么
    每秒都进来 120 了,上游就是 360 ,就算上游不限流,响应要是慢一点不得干爆你
    diantongren
        24
    diantongren  
       14 天前
    java21 ,springboot3.2 使用虚拟线程
    xrzxrzxrz
        25
    xrzxrzxrz  
       14 天前
    @Koril 用 MQ 主要是为了解耦,可以更灵活。如果写在一起,如果请求外部接口出问题了,可能拖垮对前端接口的响应。所以单独拎出来消费者。担心中间延迟,就多些消费者,保证消息不堆积,基本就不会太多的延迟。(因为你异步请求外部接口,本身就已经是默认有延迟了,不是同步操作)
    pangdundun996
        26
    pangdundun996  
       14 天前
    基本就大家说的 MQ 或者虚拟线程,瓶颈其实在外部接口,不在机器配置上
    pangdundun996
        27
    pangdundun996  
       14 天前
    @Koril 100 的 qps ,别说 MQ 了,你就是直接落库也没事啊
    brianinzz
        28
    brianinzz  
       14 天前
    还有就是一点你要搞清楚挤压不完全时因为你的 线程设置
    还要看你调用接口的 策略 好比你现在是 5 秒每个*3 就算你无限资源往上加 人家接口扛不住了变 10 秒每个或者全挂了也不是你想要的结果
    mq 更多是解耦 有了 mq 你也要确认消费的并发来调整 mq 消费者的数量或者并发数
    wupher
        29
    wupher  
       14 天前
    - 有段时间会使用 spring flow ,每个请求是个 Mono 或者 Flow ,多个请求将这些流 zip / combine 在一起,再根据业务要求返回不同接口。但无论是响应式还是 ReactiveX ,都有一些自己的问题。

    - 现在更多会用 Ktor

    - 如果你的这种请求特别多,且大,我觉得纯异步队列方案可能是最好的
    notejava
        30
    notejava  
       14 天前
    请求进来,丢给线程池去异步处理,如果任务数超出了线程池的队列长度,就暂存数据库,再起一个定时任务,定时消费数据库中的任务。
    git00ll
        31
    git00ll  
       14 天前
    前面用个 mq 接受数据,
    消费数据可以看下 Nio 相关工具,比如说 spring 的 webclient 。把自己当成网关压力给到后方 abc 服务
    ala2008
        32
    ala2008  
       14 天前
    这么高的配置不至于顶不住啊,多几个实例
    gerefoxing
        33
    gerefoxing  
       14 天前
    mq 异步处理+定时任务扫描处理失败的
    z1829909
        34
    z1829909  
       14 天前 via Android
    @Koril 只要是 mq 不管什么形式的都可能会堆积,具体是看你的业务,能不能忍受堆积,堆积了一段时间的任务是继续处理还是扔掉。
    Karte
        35
    Karte  
       14 天前
    请求量这么大肯定需要 MQ 进行存放, 至于后续你可以使用虚拟线程, 也可以试试 CompletableFuture 的函数编程方式.
    Koril
        36
    Koril  
    OP
       14 天前
    非常非常感谢大家的回复,我正在开始了解 Java 21 的虚拟线程,希望能用上
    Koril
        37
    Koril  
    OP
       14 天前
    @ala2008 之前碰到了 OOM 的问题:java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached

    因为我看程序根本没吃满内存,所以我改了 Xms Xmx Xss ,似乎都没啥用,然后我就在一个虚拟机开了俩实例,nginx 负载均衡,结果就再也没报这个错误了。
    ala2008
        38
    ala2008  
       14 天前
    @Koril 你是用 tomcat 吗?可以搜索一下 tomcat 配置优化
    litchinn
        39
    litchinn  
       14 天前   ❤️ 1
    Spring webclient 响应式请求
    Jdk21 的话虚拟线程也是不错的,这是发挥虚拟线程能力的典型场景
    感觉这个 100qps 应该用不上 mq
    asmoker
        40
    asmoker  
       14 天前
    我只关心顺序调用的中间有失败的,前面成功的接口怎么回滚?🤨
    能队列的还是队列,万一重启异步进程的请求信息不是丢了都?🤨
    yc8332
        41
    yc8332  
       14 天前
    搞个队列啊,前端请求过来就记下来然后直接返回,自己开个线程或者进程慢慢去跑所有记下来的请求
    ymy3232
        42
    ymy3232  
       14 天前
    加线程池监控慢慢调就行
    xuanbg
        43
    xuanbg  
       14 天前
    你这个不是后面的依赖前面的数据吗?直接一个方法里面挨个请求呀,用得着 @Async 么? api/a 的数据没回来,你调 api/b 时怎么传参?接口超时不应该抛出异常么?通过捕获异常来打日志就行。

    至于你每秒钟有 120 请求,这就看你的/demo 接口的并发能力了。压测一下就知道 QPS 了,然后根据需要的机器数量,在 nginx 上做个负载均衡就行。
    xianqin
        44
    xianqin  
       14 天前
    歪个楼,对于会前端的 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
        45
    Leviathann  
       14 天前
    @xianqin 全 await 不就是 java 默认的同步调用?
    coderYang
        46
    coderYang  
       14 天前
    我觉得你的描述是,1 、前端请求后,后端只要接收到请求,即可返回结果,业务操作可异步进行
    2 、异步进行时,无法确认自己线程池该给多少线程是最佳方案
    3 、链式调用的时延较高,异步等待时间过长,队列堆积

    个人想法:当无法确认外部接口的响应时间时,可通过 MQ 进行消息传递。
    三个 TOPIC ,和你的思路一致,每当有一个/demo 被请求,则直接发送消息与参数至 TOPIC-A 中,然后 CONSUMER-A 去处理。A 处理完则发消息到 B ,B 消费完则发送消息至 C 并被消费。
    通过 MQ 的方式,首先可以保证消息不丢失,且链式不出问题,日志记录、报错回滚与重试都更方便。
    至于 MQ 消费者的线程个数,这个没所谓的,基本上都是有则新建线程,等待一段时间后回收线程。
    其次如果觉得一个线程同一时间只能消费一条消息太慢的话,可以批量消费,通过 Future.get 来实现异步。
    jdk21 的虚拟线程不太懂。
    phantomzz
        47
    phantomzz  
       14 天前
    spring5 的 webclient ,基于 reactor 模型的,a 接口的 subscribe 中调 b 接口并结果落库,b 的 subscribe 中调 c 接口并结果落库,以此类推。
    workqing2023
        48
    workqing2023  
       14 天前
    这个其实主要看你是每秒都有这么多请求,还是只是偶尔有这么多请求,如果每秒都是的话,你的消费者就得大,无论是线程还是消息队列都一样,不然就会一直堆积;如果只是偶尔这么多请求,线程池小一点也没关系,慢慢消化就行了
    skallz
        49
    skallz  
       14 天前
    我也遇到过类似的需求,只不过那个需求更加消耗资源,直接丢到 serverless 了,想调几百几千次都行,哈哈,把并发的烦恼完全抛掉
    owen800q
        50
    owen800q  
       14 天前 via iPhone
    @Leviathann demo 接口調用處是異步的,不是阻塞
    ychost
        51
    ychost  
       14 天前
    虚拟线程或者用 Webflux 非阻塞来实现超高并发,当然要求底层数据库访问也要支持
    Nosub
        52
    Nosub  
       14 天前 via iPhone
    webflux 或是 RXjava ,前端就是 rxjs ,就是专门用来解决这类问题的,建议你了解下响应式编程,或是说函数式编程,不过如果你是习惯了面向对象编程可能不是太习惯,楼上很多回答都极不专业。
    mgcnrx11
        53
    mgcnrx11  
       14 天前
    @Nosub 赞同。特别是最后一句,我看到这种问题都要上 mq 都血压上升
    siweipancc
        54
    siweipancc  
       13 天前 via iPhone
    哥,信号量啊
    onedayrexgmail
        55
    onedayrexgmail  
       13 天前
    首先,你要知道你这个需求的瓶颈在哪,从你的描述中看出来,目前瓶颈在于调用的第三方接口,这些三方接口由于某些原因,可能会导致你的系统堆积,从而导致你的系统受影响,响应慢甚至导致崩溃,这个需求里面,首先第一点你要解决的问题是不要让第三方接口影响到你的系统,前端来调用你使用异步方式去调用三方接口这是一个解决方案,第二点你要解决的就是如何更高效的去调用第三方的接口。这里我说下我的方案,首先,前端调用进来我会去存一张主任务表,一个请求就是一个任务,同时有一张任务明细表,明细表就写的是你调用的 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
        56
    iiinspiration  
       12 天前
    mq+协程呗 前端的请求扔到 mq 然后协程那一直跑着就完事了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1699 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 16:45 · PVG 00:45 · LAX 08:45 · JFK 11:45
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.