密集多次 http 请求外部接口怎么操作比较好? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Dogod37
V2EX    Java

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

  •  
  •   Dogod37 2022-04-21 10:02:18 +08:00 4375 次点击
    这是一个创建于 1269 天前的主题,其中的信息可能已经有所发展或是发生改变。
    先行感谢各位大佬解答。背景如下:
    #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 上述方案如果不可行的话,有没有更合理的解决方案?期望是用户页面同步得到结果,不要异步的....会增加复杂性,搞不动了...

    多谢各位,帮忙孩子...
    第 1 条附言    2022-04-26 16:31:01 +08:00
    感谢的话就不一一给楼下各位回复了,这样帖内信息简介一点,方便其他像我一样的新手参考。

    最终还是用了 CompletableFuture+自定义全局线程池。上线两天了没啥太大问题。2 核的阿里云 ECS 表示没啥顶不住的。
    总结下:
    #1 为什么不用异步,这个系统应该是从 2010 年开始堆到现在了,之前还是用的 Struts2 和 Velocity ,今年二月份我实在受不了了,花了一个月时间改到了 Spring Boot + Freemarker 。单线程地循环逐个订单去请求外部接口的逻辑是一直存在的,用户在页面的操作就是提交后等转圈。也没个订单查询页面。而我前端是真的菜鸡,所以涉及到前端改动能躲就躲了。还有另一个系统是用 websocket 异步通知页面的,而我也认可 《异步》对于此帖的场景更为合适。
    #2 菜鸡的我第一次进行“并发”开发,所以担心遇到另外一些经验之外的情况不能及时解决。采用的方式是一批订单进来之后,先单线程循环去把每个订单存到数据库。然后再用 CompletableFuture 去并发地调用第三方接口。这样至少有问题了之后可以找到订单重推请求去调用第三方接口。(貌似这种场景本身应该也是这样处理的?)
    #3 非科班入行两年多了,前两年浪费了太多时间,现在越来越感觉到了非科班的基础知识方面的劣势,已开始从基础学起,总有收获。

    再次感谢各位的解答。啥时候系统因为这次改动崩了我回来汇报的
    24 条回复    2022-05-09 17:24:59 +08:00
    cheng6563
        1
    cheng6563  
       2022-04-21 10:19:39 +08:00
    创建线程池可以限制线程数量:new ThreadPoolExecutor(0, 60, 3, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy());
    新建线程运行任务,用一个 CountDownLatch 进行任务计数,他可以阻塞直到 30 个任务完成。

    没事别用 Future ,烦人的很
    hay0577
        2
    hay0577  
       2022-04-21 10:20:21 +08:00
    不想异步拿数据,那么 java8 的并行流应该可以满足吧
    Dogod37
        3
    Dogod37  
    OP
       2022-04-21 10:28:01 +08:00
    @cheng6563 没说清楚,Controller 层接到请求后,去调用方法并行执行这些请求,阻塞到任务全部完成后,想要这些任务又返回值,Controller 拿着这些返回值处理并返回给页面,您说的这种方式应该没有返回值?
    Dogod37
        4
    Dogod37  
    OP
       2022-04-21 10:44:44 +08:00
    @hay313955795 30 个 I/O 耗时操作的,并行流应该不太行....
    liuhan907
        5
    liuhan907  
       2022-04-21 10:53:49 +08:00 via Android
    既然你不想要异步,那并行流和线程池没有区别呀
    dqzcwxb
        6
    dqzcwxb  
       2022-04-21 10:55:32 +08:00
    并行?还要处理结果?那必然是 Completablefuture 啦
    agzou
        7
    agzou  
       2022-04-21 11:00:23 +08:00
    线程池+Future+CountDownLatch
    Dogod37
        8
    Dogod37  
    OP
       2022-04-21 11:04:46 +08:00
    @dqzcwxb 一次开 30 个线程属不属于不合理操作了?
    gesse
        9
    gesse  
       2022-04-21 11:14:17 +08:00
    设计得不好, 用户一刷新就全部 GG 了
    v2orz
        10
    v2orz  
       2022-04-21 11:14:32 +08:00
    既然你有“拆分成最多 30 份”的需求,那这么做看起来也没啥问题
    slomo
        11
    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
        12
    wolfie  
       2022-04-21 11:26:43 +08:00
    平均 5s ,高峰 20s ,最多 30 次请求,期望 30s 内,不用一次性使用 30 个线程。

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

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

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

    如果处理失败了,也可以把异常信息 写入表里(每条 提交记录,都带一个异常信息字段)。 还可以在页面上做一个重试按钮。
    Joker123456789
        16
    Joker123456789  
       2022-04-21 11:44:15 +08:00
    还有,gesse 说的,等待响应期间 如果用户刷新一下就 GG 了
    Tom7
        17
    Tom7  
       2022-04-21 11:50:11 +08:00
    不清楚具体业务,给一个体验思路,后端全异步,前端提交后可以根据提交 id 之类的循环查询结果,如果异常,通过状态跳过,避免了长时间等待,又可以直观的看到每个任务结果
    golangLover
        18
    golangLover  
       2022-04-21 12:35:25 +08:00 via Android
    可以,我自己就是经常用 completablefuture 做的
    byte10
        19
    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
        20
    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 的线程数和你第三接口的吞吐量了。
    biubiuF
        21
    biubiuF  
       2022-04-21 13:33:28 +08:00
    加个 kafka 异步吧,然后用户请求的时候加个 actionId ,这个 id 1 分钟刷新一次,不然有的用户转圈等待的时候会一直刷新(比如我),可能直接打满 fd
    dqzcwxb
        22
    dqzcwxb  
       2022-04-21 23:12:51 +08:00
    @Dogod37 #8 11 楼说的很对,你开个线程池就行,就算是 300 个线程都随你
    git00ll
        23
    git00ll  
       2022-04-21 23:43:13 +08:00
    nio ,你想开多少都可以,还不用担心线程太多。不过你调用的三方服务也是够垃的。
    liian2019
        24
    liian2019  
       2022-05-09 17:24:58 +08:00
    量不大,业务不是太重要,用线程池去并发调用还行。量大的话这种场景最好还是别用多线程,技术上很容易实现搞些 CompletableFuture 什么的,对 C 的同步接口使用线程池要考虑的东西太多,如线程池参数怎么配置,下游接口能不能扛得住你这并发调用,线上跑起来线程池满了咋办,现在可能 150S ,请求堵住了,很可能就不止 150S 了,甚至整个系统都会受影响。其实还是提交和结果查询分两个接口来的比较好。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     872 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 21:33 PVG 05:33 LAX 14:33 JFK 17:33
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86