一种接口依赖关系分层方案 | 京东云技术团队

news2024/11/14 16:40:36

1、背景

到店商详迭代过程中,需要提供的对外能力越来越多,如预约日历、附近门店、为你推荐等。这其中不可避免会出现多个上层能力依赖同一个底层接口的场景。最初采用的方案是对外API入口进来后获取对应的能力,并发调用多项能力,由能力层调用对应的数据链路,进行业务处理。然而,随着接入功能的增多,这种情况导致了底层数据服务的重复调用,如商品配置信息,在一次API调用过程中重复调了3次,当流量增大或能力项愈多时,对底层服务的压力会成倍增加。

正值618大促,各方接口的调用都会大幅度增加。通过梳理接口依赖关系来减少重复调用,对本系统而言,降低了调用数据接口时的线程占用次数,可以有效降级CPU。对调用方来说,减少了调用次数,可减少调用方的资源消耗,保障底层服务的稳定性。

原始调用方式:

2、优化

基于上述问题,采用底层接口依赖分层调用的方案。梳理接口依赖关系,逐层向上调用,注入数据,如此将同一接口的调用抽取到某层,仅调用一次,即可在整条链路使用。

改进调用方式:

只要分层后即可在每层采用多线程并发的方式调用,因为同一层级中的接口无先后依赖关系。

3、如何分层?

接下来,如何梳理接口层级关系就至关重要。

接口梳理分层流程如下:

第一步:构建层级结构

首先获取到能力层依赖项并遍历,然后调用生成数据节点方法。方法流程如下:构建当前节点,检测循环依赖(存在循环依赖会导致栈溢出),获取并遍历节点依赖项,递归生成子节点,存放子节点。

第二步:节点平铺

定义Map维护平铺结构,调用平铺方法。方法流程如下:遍历层级结构,判断当前节点是否已存在map中,存在时与原节点比较将层级大的节点放入(去除重复项),不存在时直接放入即可。然后处理子节点,递归调用平铺方法,处理所有节点。

第三步:分层(分组排序)

流处理平铺结构,处理层级分组,存储在TreeMap中维护自然排序。对应key中的数据节点Set需用多线程并发调用,以保证链路调用时间

1 首先,定义数据结构用于维护调用链路

Q1:为什么需要定义祖先节点?

A1:为了判断接口是否存在循环依赖。如果接口存在循环依赖而不检测将导致调用栈溢出,故而在调用过程中要避免并检测循环依赖。在遍历子节点过程中,如果发现当前节点的祖先已经包含当前子节点,说明依赖关系出现了环路,即循环依赖,此时抛异常终止后续流程避免栈溢出。

public class DataNode {
    /**
     * 节点名称
     */
    private String name;
    /**
     * 节点层级
     */
    private int level;
    /**
     * 祖先节点
     */
    private List<String> ancestors;
    /**
     * 子节点
     */
    private List<DataNode> children;
}

2 获取能力层的接口依赖,并生成对应的数据节点

Q1:生成节点时如何维护层级?

A1:从能力层依赖开始,层级从1递加。每获取一次底层依赖,底层依赖所生成的节点层级即父节点层级+1。

/**
 * 构建层级结构
 *
 * @param handlers 接口依赖
 * @return 数据节点集
 */
private List<DataNode> buildLevel(Set<String> handlers) {
    List<DataNode> result = Lists.newArrayList();

    for (String next : handlers) {
        DataNode dataNode = generateNode(next, 1, null, null);
        result.add(dataNode);
    }
    return result;
}

/**
 * 生成数据节点
 *
 * @param name 节点名称
 * @param level 节点层级
 * @param ancestors 祖先节点(除父辈)
 * @param parent 父节点
 * @return DataNode 数据节点
 */
private DataNode generateNode(String name, int level, List<String> ancestors, String parent) {
    AbstractInfraHandler abstractInfraHandler = abstractInfraHandlerMap.get(name);
    Set<String> infraDependencyHandlerNames = abstractInfraHandler.getInfraDependencyHandlerNames();
    // 根节点
    DataNode dataNode = new DataNode(name);
    dataNode.setLevel(level);
    dataNode.putAncestor(ancestors, parent);
    if (CollectionUtils.isNotEmpty(dataNode.getAncestors()) && dataNode.getAncestors().contains(name)) {
        throw new IllegalStateException("依赖关系中存在循环依赖,请检查以下handler:" + JsonUtil.toJsonString(dataNode.getAncestors()));
    }
    if (CollectionUtils.isNotEmpty(infraDependencyHandlerNames)) {
        // 存在子节点,子节点层级+1
        for (String next : infraDependencyHandlerNames) {
            DataNode child = generateNode(next, level + 1, dataNode.getAncestors(), name);
            dataNode.putChild(child);
        }
    }
    return dataNode;
}

层级结构如下:

3 数据节点平铺(遍历出所有后代节点)

Q1:如何处理接口依赖过程中的重复项?

A1:遍历所有的子节点,将所有子节点平铺到一层,平铺时如果节点已经存在,比较层级,保留层级大的即可(层级大说明依赖位于更底层,调用时要优先调用)。

/**
 * 层级结构平铺
 *
 * @param dataNodes 数据节点
 * @param dataNodeMap 平铺结构
 */
private void flatteningNodes(List<DataNode> dataNodes, Map<String, DataNode> dataNodeMap) {
    if (CollectionUtils.isNotEmpty(dataNodes)) {
        for (DataNode dataNode : dataNodes) {
            DataNode dataNode1 = dataNodeMap.get(dataNode.getName());
            if (Objects.nonNull(dataNode1)) {
                // 存入层级大的即可,避免重复
                if (dataNode1.getLevel() < dataNode.getLevel()) {
                    dataNodeMap.put(dataNode.getName(), dataNode);
                }
            } else {
                dataNodeMap.put(dataNode.getName(), dataNode);
            }
            // 处理子节点
            flatteningNodes(dataNode.getChildren(), dataNodeMap);
        }
    }
}

平铺结构如下:

4 分层(分组排序)

Q1:如何分层?

A1:节点平铺后已经去重,此时借助TreeMap的自然排序特性将节点按照层级分组即可。

/**
 * @param dataNodeMap 平铺结构
 * @return 分层结构
 */
private TreeMap<Integer, Set<DataNode>> processLevel(Map<String, DataNode> dataNodeMap) {
    return dataNodeMap.values().stream().collect(Collectors.groupingBy(DataNode::getLevel, TreeMap::new, Collectors.toSet()))
}

分层如下:

1.根据分层TreeMap的key倒序即为调用的层级顺序

对应key中的数据节点Set需用多线程并发调用,以保证链路调用时间

4、分层级调用

梳理出调用关系并分层后,使用并发编排工具调用即可。这里梳理的层级关系,level越大,表示越优先调用。

这里以京东内部并发编排框架为例,说明调用流程:

/**
 * 构建编排流程
 *
 * @param infraDependencyHandlers 依赖接口
 * @param workerExecutor 并发线程
 * @return 执行数据
 */
public Sirector<InfraContext> buildSirector(Set<String> infraDependencyHandlers, ThreadPoolExecutor workerExecutor) {
    Sirector<InfraContext> sirector = new Sirector<>(workerExecutor);
    long start = System.currentTimeMillis();
    // 依赖顺序与执行顺序相反
    TreeMap<Integer, Set<DataNode>> levelNodes;
    TreeMap<Integer, Set<DataNode>> cacheLevelNodes = localCacheManager.getValue("buildSirector");
    if (Objects.nonNull(cacheLevelNodes)) {
        levelNodes = cacheLevelNodes;
    } else {
        levelNodes = getLevelNodes(infraDependencyHandlers);
        ExecutorUtil.executeVoid(asyncTpExecutor, () -> localCacheManager.putValue("buildSirector", levelNodes));
    }
    log.info("buildSirector 梳理依赖关系耗时:{}", System.currentTimeMillis() - start);
    // 最底层接口执行
    Integer firstLevel = levelNodes.lastKey();
    EventHandler[] beginHandlers = levelNodes.get(firstLevel).stream().map(node -> abstractInfraHandlerMap.get(node.getName())).toArray(EventHandler[]::new);
    EventHandlerGroup group = sirector.begin(beginHandlers);

    Integer lastLevel = levelNodes.firstKey();
    for (int i = firstLevel - 1; i >= lastLevel; i--) {
        EventHandler[] thenHandlers = levelNodes.get(i).stream().map(node -> abstractInfraHandlerMap.get(node.getName())).toArray(EventHandler[]::new);
        group.then(thenHandlers);
    }
    return sirector;
}

5、 个人思考

  1. 作为接入内部RPC、Http接口实现业务处理的项目,在使用过程中要关注调用链路上的资源复用,尤其长链路的调用,要深入考虑内存资源的利用以及对底层服务的压力。

  2. 要关注对外服务接口与底层数据接口的响应时差,分析调用逻辑与流程是否合理,是否存在优化项。

  3. 多线程并发调用多个平行数据接口时,如何使得各个线程的耗时方差尽可能小?

作者:京东零售 王江波

来源:京东云开发者社区

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/690535.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Vue】组件高级

文章目录 组件高级一、watch 监听器二、组件的生命周期2.1 组件运行的过程2.2 监听组件的不同时刻2.3 监听组件的更新2.4 主要生命周期函数 三、组件之间的数据共享3.1 组件关系及数据共享3.2 父子组件数据共享3.3 兄弟组件数据共享3.4 后代组件数据共享3.5 vuex 四、全局配置 …

基于Thinkphp6框架全新UI的AI网址导航系统源码

✨ 源码介绍 2023全新UI的AI网址导航系统源码&#xff0c;基于thinkphp6框架开发的 AI 网址导航是一个非常实用的工具&#xff0c;它能够帮助用户方便地浏览和管理自己喜欢的网站。 相比于其他的 AI 网址导航&#xff0c;这个项目使用了更加友好和易用的 ThinkPHP 框架进行搭建…

基于3×3耦合器的干涉型光纤传感器信号解调技术研究-Matlab代码

一、引言 光纤耦合器是一种使光信号在特殊结构的耦合区内发生耦合&#xff0c;将功率再分配的无源器件。它在光纤传感和光通信等领域有着广泛的应用&#xff0c;在耦合过程中&#xff0c;光信号的频谱成分不变&#xff0c;只是信号的光功率发生变化。1980年&#xff0c;K&…

2023双态IT北京用户大会回顾(二) | 云内外一体化智能监控构建之路

文末附有本场专题演讲视频 2023第五届双态IT北京用户大会擎创科技专场演讲回顾&#xff08;二&#xff09;&#xff1a;《云内外一体化智能监控构建之路》演讲嘉宾&#xff1a;擎创科技产品线总监 殷传旺 一、前言&#xff1a;企业运维自然推进or独立发展&#xff1f; 我们从…

WebAssembly能否取代Docker?

“如果WebAssembly&#xff08;Wasm&#xff09;在几年前出现&#xff0c;Docker可能就不会出现了。因为它是一项非常强大的跨平台技术&#xff0c;可以让我们使用不同的编程语言来编写跨平台应用程序。Docker的原始动力之一就是提供一个跨平台部署和应用的方法。” -- Solomon…

lesson6 ZIgbee网络特性抓包分析

目录 ZIgbee网络特性抓包分析 理论部分&#xff1a; 实验部分&#xff1a; 入网前相关数据帧&#xff08;仅单个设备供电&#xff09;&#xff1a; 首先以路由器和协调器为例&#xff1a;&#xff08;帧的类别从头部的Type可以看到&#xff0c;在四大帧类别中根据不同作用也…

Vue 项目build打包发布到github pages ,刷新页面首页正常打开,子路由报错404的解决方法

一、问题效果如下&#xff1a; 二、解决方法 1、切换到gh-pages分支 2、在index.html同级新增404.html文件&#xff0c;其内容与index.html一样 三、解决后的效果如下&#xff1a; 相关文章 Vue3 Vite Ts开源后台管理系统模板 基于ElementUi或AntdUI再次封装基础组件文档 基…

Java版本spring cloud 电子招标采购系统源码:营造全面规范安全的电子招投标环境,促进招投标市场健康可持续发展

营造全面规范安全的电子招投标环境&#xff0c;促进招投标市场健康可持续发展 传统采购模式面临的挑战 一、立项管理 1、招标立项申请 功能点&#xff1a;招标类项目立项申请入口&#xff0c;用户可以保存为草稿&#xff0c;提交。 2、非招标立项申请 功能点&#xff1a;非招标…

VueX用法快速回顾(简洁纯干货)

vuex基础结构 代码结构 vuex的完整结构长这样&#xff0c;其包含了state、mutations、actions、modules及getters5个部分。 import Vue from vue import Vuex from vuexVue.use(Vuex)const store new Vuex.Store({state: {},mutations: {},actions:{},modules:{},getters:{…

mysql数据库 查询出的字段值是 科学计数法数字,数字转换为易于理解的形式

目录 1 问题2 实现 1 问题 数据库查询出来的可能是这种&#xff0c;如何将数字转换为易于理解的形式&#xff1b; 2 实现

ppt文件全部删除怎么恢复?别急,这些方法能挽救

PPT文件全部删除怎么恢复&#xff1f;如果您不小心将PPT文件删除&#xff0c;不要担心&#xff0c;您可能还有机会将其恢复。在下面的文章中&#xff0c;我们将介绍一些技巧来恢复已删除的PPT文件。 第一部分&#xff1a;关于PPT文件 PPT文件是指Microsoft PowerPoint软件创建…

互联网医院开发|互联网医院系统源码技术实现

健康医疗这几年一直成为人们比较关注的问题&#xff0c;互联网医院系统通过信息化技术精简了预约挂号&#xff0c;优化了医生的诊断流程&#xff0c;通过互联网医疗&#xff0c;患者复诊或医疗咨询需求都可以在线上进行&#xff0c;这种快捷的线上问诊方式缓解一线医务人员的压…

【成都】EFDC建模方法及在地表水环境评价、水源地划分、排污口论证中实践技术应用

为了定量地描述地表水环境质量与污染排放之间的动态关系&#xff0c;EFDC、MIKE、Delft3D、Qual2K等数值模型被广泛应用在环境、水务、海洋等多个领域。Environmental Fluid Dynamics Code&#xff08;EFDC&#xff09;是一款用于模拟江河&#xff0c;湖泊&#xff0c;河口&…

Linux 中利用设备树点灯

系列文章目录 第一章 Linux 中内核与驱动程序 第二章 Linux 设备驱动编写 &#xff08;misc&#xff09; 第三章 Linux 设备驱动编写及设备节点自动生成 &#xff08;cdev&#xff09; 第四章 Linux 平台总线platform与设备树 第五章 Linux 设备树中pinctrl与gpio&#xff08;…

通过一道题再了解一点Array原型上的every方法

一、例题 首先我们先来看这么一系列关于every使用的题&#xff1a; console.log(1, [].every(() > false)) console.log(2, [,,].every(() > false)) console.log(3, [undefined].every(() > false)) console.log(4, [false].every(() > false)) console.log(5, […

【深度学习】最全的十九种损失函数汇总

tensorflow和pytorch很多都是相似的&#xff0c;这里以pytorch为例。 文章目录 一、L1范数损失 L1Loss二、均方误差损失 MSELoss三、交叉熵损失 CrossEntropyLoss四、KL 散度损失 KLDivLoss五、二进制交叉熵损失 BCELoss六、BCEWithLogitsLoss七、MarginRankingLoss八、HingeEm…

git rebase和git merge在团队协作开发中的用法

git rebase和git merge是在日常开发中常用的用于分支合并的命令&#xff0c;也是非常容易误用的两个命令。本文将通过图文的方式去详解二者之间的区别。 git merge git merge会为本次的合并过程生成一条新的commit&#xff0c;并将该commit添加到目的分支上。通常用于将featu…

【Tensorflow object detection API + 微软NNI】图像分类问题完成自动调参,进一步提升模型准确率!

1. 背景&目标 利用Tensorflow object detection API开发并训练图像分类模型&#xff08;例如&#xff0c;Mobilenetv2等&#xff09;&#xff0c;自己直接手动调参&#xff0c;对于模型的准确率提不到极致&#xff0c;利用微软NNI自动调参工具进行调参&#xff0c;进一步提…

Keep通过IPO聆讯,3年烧掉16亿

“运动科技第一股”来了&#xff01; 6月21日&#xff0c;线上健身平台的运营方、北京卡路里科技有限公司&#xff08;下称“Keep”&#xff09;已正式通过聆讯&#xff0c;股票代码为810342.HK。 Keep是一家在线健身平台&#xff0c;主要产品包括在线健身内容、智能健身设备…

【python百炼成魔】python之内置函数range

前言 文章目录 前言内置函数 range()三种创建方式1. 只有一个参数的情况2. 给定两个参数的情况3. 三个参数都给定的时候 使用in和not in 来判断指定的整数是否存在1. 判断range生成的序列中是否存在指定的值2. in 和not in 不与range结合的情况 总结 内置函数 range() range()函…