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

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

  •  
  •   Dogod37 · 2022-04-21 10:02:18 +08:00 · 3346 次点击
    这是一个创建于 707 天前的主题,其中的信息可能已经有所发展或是发生改变。
    先行感谢各位大佬解答。背景如下:
    #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 ,烦人的很
    hay313955795
        2
    hay313955795  
       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   ·   我们的愿景   ·   实用小工具   ·   1006 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 36ms · UTC 22:01 · PVG 06:01 · LAX 15:01 · JFK 18:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.