• 让API并行调用变得如丝般顺滑的绝招


    当数据量较大的时候,都会通过分库分表来拆分,分担读写的压力。分库分表后比较麻烦的就是查询的问题,如果不是直接根据分片键去查询的话,需要对多个表进行查询。

    在一些复杂的业务场景下,比如订单搜索,除了订单号,用户,商家 这些常用的搜索条件,可能还有时间,商品等等。

    目前常见的做法将数据同步到ES这类搜索框架中进行查询,然后通过搜出来的结果,一般是主键ID, 再去具体的数据表中查询完整的数据,组装返回给调用方。

    比如下面这段代码,首先查询出文章信息,然后根据文章中的用户ID去查询用户的昵称。

    List<ArticleBO> articleBos = articleDoPage.getRecords().stream().map(r -> {
        String nickname = userManager.getNickname(r.getUserId());
        return articleBoConvert.convertPlus(r, nickname);
    }).collect(Collectors.toList());
    

    如果文章有10条数据,那么就需要调用10次用户服务提供的接口,而且是同步调用操作。

    当然我们也可以用并行流来实现并发调用,代码如下:

    List<ArticleBO> articleBos = articleDoPage.getRecords().parallelStream().map(r -> {
        String nickname = userManager.getNickname(r.getUserId());
        return articleBoConvert.convertPlus(r, nickname);
    }).collect(Collectors.toList());
    

    并行流的优点很明显,代码不用做特别大的改动。需要注意如果用并行流,最好单独定义一个ForkJoinPool。

    除了用并行流,还可以使用批量查询的方式来提高性能,降低RPC的调用次数,代码如下:

    List<Long> userIds = articleDoPage.getRecords().stream().map(article -> article.getUserId()).collect(Collectors.toList());
    Map<Long, String> nickNameMap = userManager.queryByIds(userIds).stream().collect(Collectors.toMap(UserResponse::getId, UserResponse::getNickname));
    List<ArticleBO> articleBos = articleDoPage.getRecords().stream().map(r -> {
        String nickname = nickNameMap.containsKey(r.getUserId()) ? nickNameMap.get(r.getUserId()) : CommonConstant.DEFAULT_EMPTY_STR;
        return articleBoConvert.convertPlus(r, nickname);
    }).collect(Collectors.toList());
    

    但批量查询还是同步模式,下面介绍如果使用CompletableFuture来实现异步并发调用,直接用原生的CompletableFuture也可以,但是编排能力没有那么强,这里我们选择一款基于CompletableFuture封装的并行编排框来实现,详细介绍查看我之前的这篇文章:https://mp.weixin.qq.com/s/3EE8ccydK16gC1oY4AWnoA

    稍微做了下封装,提供了更方便使用的工具类来实现并发调用多个接口的逻辑。

    第一种方式,适用于比如从ES查出了一批ID, 然后根据ID去数据库中或者调用RPC查询真实数据,最后得到一个Map,可以根据Key获取对应的数据。

    内部是多线程并发调用,会等到结果全部返回。

    public Object aggregationApi() {
        long s = System.currentTimeMillis();
        List<String> ids = new ArrayList<>();
        ids.add("1");
        ids.add("2");
        ids.add("3");
        Map<String, UserResponse> callResult = AsyncTemplate.call(ids, id -> {
            return userService.getUser(id);
        }, u -> u.getId(), COMMON_POOL);
        long e = System.currentTimeMillis();
        System.out.println("耗时:" + (e-s) + "ms");
        return "";
    }
    

    另一个场景就是API聚合的场景,需要并行调用多个接口,将结果进行组装。

    List<AsyncCall> params = new ArrayList<>();
    AsyncCall<Integer, Integer> goodsQuery = new AsyncCall("goodsQuery", 1);
    params.add(goodsQuery);
    AsyncCall<String, OrderResponse> orderQuery = new AsyncCall("orderQuery", "100");
    params.add(orderQuery);
    UserQuery q = new UserQuery();
    q.setAge(18);
    q.setName("yinjihuan");
    AsyncCall<UserQuery, UserResponse> userQuery = new AsyncCall("userQuery", q);
    params.add(userQuery);
    AsyncTemplate.call(params, p -> {
        if (p.getTaskId().equals("goodsQuery")) {
            AsyncCall<Integer, Integer> query = p;
            return goodsService.getGoodsName(query.getParam());
        }
        if (p.getTaskId().equals("orderQuery")) {
            AsyncCall<String, OrderResponse> query = p;
            return orderService.getOrder(query.getParam());
        }
        if (p.getTaskId().equals("userQuery")) {
            AsyncCall<UserQuery, UserResponse> query = p;
            return userService.getUser(query.getParam());
        }
        return null;
    });
    

    AsyncCall中定义参数和响应的类型,响应结果会在执行完后会自动设置到AsyncCall中。在call方法中需要根据taskId去做对应的处理逻辑,不同的taskId调用的接口不一样。

    源码参考:https://github.com/yinjihuan/kitty

    关于作者:尹吉欢,简单的技术爱好者,《Spring Cloud微服务-全栈技术与案例解析》, 《Spring Cloud微服务 入门 实战与进阶》作者, 公众号猿天地发起人。

  • 相关阅读:
    用脚本实现对pm2.5 数据的获取
    虚拟机下的快盘空间分配方案
    C++中new的用法
    只在堆或栈上生成对象
    Virtualbox识别USB设备
    最长公共字序列
    gdb 打印vector 等stl数据
    LC_CTYPE: cannot change locale
    字符串压缩
    对象的概念
  • 原文地址:https://www.cnblogs.com/yinjihuan/p/14205855.html
Copyright © 2020-2023  润新知